Skip to content

Add IME language selection to InputOptions for CJK support#7914

Open
rustbasic wants to merge 23 commits intoemilk:mainfrom
rustbasic:patch167
Open

Add IME language selection to InputOptions for CJK support#7914
rustbasic wants to merge 23 commits intoemilk:mainfrom
rustbasic:patch167

Conversation

@rustbasic
Copy link
Copy Markdown
Contributor

Add IME language selection to InputOptions for CJK support

Overview
This PR adds a new configuration option to select specific IME (Input Method Editor) processing modes, particularly for CJK (Chinese, Japanese, Korean) languages. This allows for more specialized handling of character composition depending on the selected language.

Changes

  • Introduced ImeLanguage enum (None, Korean, Japanese, Chinese) with serde support.
  • Added ime_language field to InputOptions struct.
  • Updated InputOptions::ui to include a ComboBox for easy language selection in the settings panel.

Why this is needed
Proper CJK support often requires different composition logic depending on the language. By allowing users to explicitly select their IME mode, we can provide a more robust input experience for international users.

In addition to #7898, a bug in Korean input on mobile (Android) has been fixed.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 16, 2026

Preview available at https://egui-pr-preview.github.io/pr/7914-patch167
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.

View snapshot changes at kitdiff

@umajho umajho mentioned this pull request Mar 11, 2026
1 task
@umajho
Copy link
Copy Markdown
Contributor

umajho commented Mar 18, 2026

Hello.

In my previous comment, I mentioned that I suspected the root cause of the IME behavior differences on Windows lies in winit. About a week ago, I was finally able to debug on Windows and confirmed that this is indeed the case.
Since then, I've submitted another PR (#7967) that avoids the need to distinguish between the so called “IME languages”.

In addition, that PR addresses several issues not covered here. One of them involves a Korean IME case (Hanja confirmation) where simply setting the IME language to Korean does not help, as its behavior more closely resembles that of typical Japanese IMEs.
This further suggests that differentiating by “IME language” is the wrong abstraction: there can be multiple distinct IME behaviors even within the same language.

I would appreciate your feedback on my PR (#7967). If you have time to review it, I’d be grateful for any comments or suggestions for improvement (similar to the concerns I raised in my previous comment).

Separately, you mentioned that this PR also fixes another IME-related issue. Could you provide more details about that? From the changes, I can tell it relates to a Korean IME using the Cheonjiin layout on Android (web?), but I’m unclear on what the exact issue was and which specific IME is affected. As I understand it, multiple IMEs support this layout.

Thank you.

@rustbasic
Copy link
Copy Markdown
Contributor Author

Regarding why language-specific IME selection was previously unnecessary on certain OSs:

Until now, the reason we haven't needed to manually select IMEs for each language on specific operating systems is that users typically install an OS tailored to a specific language. During this process, the language-specific IME is installed either automatically or manually, allowing users to use it without much concern.

However, since egui is a cross-platform framework that operates across all platforms, it must implement its own IME handling logic internally. Given the unique characteristics of CJK (Chinese, Japanese, and Korean) languages and the existence of various IMEs within each language, it is extremely difficult to handle everything with a single, unified function.

To achieve a "one-function-fits-all" approach, we must first complete the handling logic for every language and IME, and then implement a system to automatically recognize them. We cannot skip these essential intermediate steps and jump straight to a perfect, universal solution.

It would be ideal if a library like winit handled everything, but we can't just wait around for that.

@umajho
Copy link
Copy Markdown
Contributor

umajho commented Mar 21, 2026

Regarding why language-specific IME selection was previously unnecessary on certain OSs:

Until now, the reason we haven't needed to manually select IMEs for each language on specific operating systems is that users typically install an OS tailored to a specific language. During this process, the language-specific IME is installed either automatically or manually, allowing users to use it without much concern.

However, since egui is a cross-platform framework that operates across all platforms, it must implement its own IME handling logic internally. Given the unique characteristics of CJK (Chinese, Japanese, and Korean) languages and the existence of various IMEs within each language, it is extremely difficult to handle everything with a single, unified function.

To achieve a "one-function-fits-all" approach, we must first complete the handling logic for every language and IME, and then implement a system to automatically recognize them. We cannot skip these essential intermediate steps and jump straight to a perfect, universal solution.

It would be ideal if a library like winit handled everything, but we can't just wait around for that.


Until now, the reason we haven't needed to manually select IMEs for each language on specific operating systems is that users typically install an OS tailored to a specific language. During this process, the language-specific IME is installed either automatically or manually, allowing users to use it without much concern.

I don't quite understand this. what exactly is the “concern” being referred to?

Are you suggesting that users typically use IMEs matching their system language? If so, that distinction isn't particularly meaningful. Whether the system language matches the IME language doesn't affect how IMEs function.

Many users, myself included, run their systems in English while using IMEs for other languages. If an OS couldn't support multiple IMEs simultaneously, some would fail under an English system, which would be considered a bug. A mature platform must ensure that IMEs work reliably regardless of the system language. Fortunately, egui targets only mature platforms (Linux, macOS, Windows, and Web), where this is generally expected.


However, since egui is a cross-platform framework that operates across all platforms, it must implement its own IME handling logic internally. Given the unique characteristics of CJK (Chinese, Japanese, and Korean) languages and the existence of various IMEs within each language, it is extremely difficult to handle everything with a single, unified function.

If handling IMEs for multiple languages isn't inherently problematic on a single platform, it's not clear why expanding to multiple platforms would introduce language-specific issues. The real differences lie between platforms, which can be handled using mechanisms like cfg(target_os = …).


To achieve a "one-function-fits-all" approach, we must first complete the handling logic for every language and IME, and then implement a system to automatically recognize them. We cannot skip these essential intermediate steps and jump straight to a perfect, universal solution.

The reason we should adopt a “universal solution”, regardless of the language or IME a user is using, is precisely because we CAN'T and SHOULD'T attempt to handle every language and IME individually.

  1. Language detection is effectively impossible.

    • As an IME user, my system is in English. There is no reliable way to determine which language my IME is using based on that alone.
    • Some Chinese IMEs report Chinese characters as Preedit text. In such cases, it may be impossible to distinguish whether the user is composing Chinese or Japanese.
    • Even if we could detect which IME is being used (Spoiler: we generally can't), how would we determine its target language? An IME with “Hangul” in its name is clearly Korean, but what about something ambiguously named like “Foobar”? Maintaining a mapping of IMEs to languages (which we shouldn't do) would still fail for newly introduced IMEs.
  2. Even if language detection were possible, it would still be unreliable. Knowing the language of an IME does not provide sufficient information to implement correct behavior.

    • Different IMEs for the same language can behave differently. For example, even with the same layout (Cheonjiin) on the same platform (Android), we've observed three distinct behaviors across three IMEs (Gboard, SwiftKey, and another IME with a bug addressed in this PR).
    • As a result, a fix targeting one IME could break another, even within the same language.
  3. IME detection is both undesirable and often infeasible.

    • The action of probing which IME a user is using might concerns some users. They are personal data.
    • It is just not feasible at all in web environments or other sandboxed contexts.
  4. Even if IME detection were possible, egui should not be responsible for workarounds that are specific to particular IMEs. Once we introduce a workaround for one IME, we effectively open the door to working around all IMEs individually.

Therefore, in egui, IME handling should be based solely on the general interface (i.e., a “universal solution”), rather than targeting specific IMEs or languages.

The current IME handling logic already follows a “one-function-fits-all” approach. The “language selection” proposed in this PR breaks it, yet can't be pushed further: because, as explained above, handling every language and IME individually is infeasible. In my opinion, it only moves the codebase away from the “one-function-fits-all” goal rather than toward it.


It would be ideal if a library like winit handled everything, but we can't just wait around for that.

If there are bugs upstream, the proper approach is to fix them there. Temporary workarounds in egui may be necessary, but they shouldn't be treated as permanent solutions.

The main reason I haven't contributed fixes to winit is that egui currently depends on winit 0.30, while the latest version is 0.31 (beta). The changes between these versions are substantial, and backporting fixes would be quite cumbersome. It seems more practical to wait until egui upgrades to winit 0.31.

@umajho
Copy link
Copy Markdown
Contributor

umajho commented Mar 21, 2026

on macOS Safari, the builtin Chinese IME (Shuangpin) is broken by default with this PR:
2026-03-21 11 40 21 PM

on macOS Safari, the builtin Japanese IME is broken is broken by default with this PR:
2026-03-21 11 41 39 PM

on Windows, the builtin Chinese IME (Shuangpin) is broken by default with this PR:
2026-03-21 11 54 59 PM

Since these IMEs previously worked out of the box prior to this PR, they should also work out of the box after this PR as well.

As I have pointed out here, most developers will not bother integrating this language selection thing, and most affected users are unlikely to discover it even if it is integrated in the app they are using.
Even if we assume most developers do integrate it and most users discover it, the existence of “any developer or user who does not” means this remains a regression for those affected users: It worked prior to this PR, and no longer works after it.


Note that I am not implying any prioritization of languages. The point is simply that the current approach proposed by this PR is not that feasible.

@umajho
Copy link
Copy Markdown
Contributor

umajho commented Mar 22, 2026

I'm not home and can't verify this, so this is merely a baseless speculation:

By any chance the IME you are addressing here handles the batchim situation by sending a backspace event to delete the first character. And the reason it is broken is because eframe/web incorrectly suppresses that backspace event in such cases?

@rustbasic
Copy link
Copy Markdown
Contributor Author

@umajho

Since I rely on translators or AI translation, it can be difficult to convey precise nuances.

The "Cheonjiin keyboard" does not trigger a backspace event; instead, it returns Preedit("", 1, 1) to cancel the previously completed character. Even within the Korean language alone, there are inconsistencies across Windows, WASM, Android, and the "Cheonjiin keyboard" layout.

The primary issue currently lies in the conflicts between CJK languages, so I would like to keep the Korean logic separate for the time being. These IME bugs have remained unresolved for years, and since I slightly mitigated them a year ago, they have stayed in the same state. Fixing one side often leads to issues on the other. It seems wiser to address the bugs by separating CJK first and then determine if they can be merged later.

I may be speaking from my own limitations, as I can only test Korean on Windows, WASM, and Android. Perhaps umajho can handle this, as they might be able to test most platforms and languages. Applying #7967 + #7983 (while maintaining #4137) shows a lot of improvement, so I see a lot of potential there.

Let's make this happen!

@umajho
Copy link
Copy Markdown
Contributor

umajho commented Mar 22, 2026

Since I rely on translators or AI translation, it can be difficult to convey precise nuances.

I can definitely relate. I'm not very confident in my English writing either.
Whenever I finish writing comments, I usually run them through an LLM for polishing. Then I selectively adopt revisions that feel more readable while still preserving my original intent. I end up spending quite a bit of time writing and editing comments, just trying to reduce the loss of nuance during this process.

Let's make this happen!

I’ll do my best to fix this Cheonjiin bug while trying not to introduce IME-specific logic.

The "Cheonjiin keyboard" does not trigger a backspace event; instead, it returns Preedit("", 1, 1) to cancel the previously completed character. Even within the Korean language alone, there are inconsistencies across Windows, WASM, Android, and the "Cheonjiin keyboard" layout.

It would be very helpful if you could reproduce the bug while logging the following 6 events (The more informative, the better):

runner_ref.add_event_listener(&input, "input", on_input)?;
runner_ref.add_event_listener(&input, "compositionstart", on_composition_start)?;
runner_ref.add_event_listener(&input, "compositionupdate", on_composition_update)?;
runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?;
// The canvas doesn't get keydown/keyup events when the text agent is focused,
// so we need to forward them to the runner:
runner_ref.add_event_listener(&input, "keydown", super::events::on_keydown)?;
runner_ref.add_event_listener(&input, "keyup", super::events::on_keyup)?;

Once you have that, please paste the logs here.

To access the console on Android Chrome, you can use:
https://developer.chrome.com/docs/devtools/remote-debugging

Thanks.

@rustbasic
Copy link
Copy Markdown
Contributor Author

rustbasic commented Mar 22, 2026

@umajho

For reference, the on_ime_korean() function in #7914 has resolved all Korean input issues I am aware of across Windows, WASM, and the "Cheonjiin keyboard"(Android) layout.

** Below are the logs generated while typing the word "않았다" **


ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Meta', code='MetaLeft', is_composing=false, key_code=91
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=false, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:88: on_composition_start: data='Some("")'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㅇ', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='ㅇ'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='이', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='이'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='아', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='아'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='안', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='안'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='안', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='안'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:124: on_composition_end: data='안'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=false
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=false, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:88: on_composition_start: data='Some("")'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㅅ', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='ㅅ'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㅅ', selection_start=Some(1)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='않', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='않'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='않', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='않'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:124: on_composition_end: data='않'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=false
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=false, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:88: on_composition_start: data='Some("")'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㅇ', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='ㅇ'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='이', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='이'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='아', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='아'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='앗', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='앗'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='앟', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='앟'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='았', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='았'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='았', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='았'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:124: on_composition_end: data='았'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=false
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=false, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:88: on_composition_start: data='Some("")'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㄷ', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='ㄷ'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='디', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='디'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='다', selection_start=Some(0)
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='다'
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-9c02abe29a2f63ed.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:124: on_composition_end: data='다'

@rustbasic
Copy link
Copy Markdown
Contributor Author

@umajho

I noticed that I am mistakenly assigning input.selection_start() to the end value. However, since both start and end are 1 at that specific point, there is no functional difference in the current behavior.

Below are the logs generated while typing "않았다":

[eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Enter', code='', is_composing=false
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=false, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:88: on_composition_start: data='Some("")'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㅇ', selection_start=Some(0), selection_end=Some(0)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='ㅇ'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='이', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='이'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='아', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='아'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='안', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='안'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='안', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='안'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:125: on_composition_end: data='안'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=false
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=false, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:88: on_composition_start: data='Some("")'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㅅ', selection_start=Some(0), selection_end=Some(0)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='ㅅ'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㅅ', selection_start=Some(1), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='않', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='않'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='않', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='않'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:125: on_composition_end: data='않'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=false
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=false, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:88: on_composition_start: data='Some("")'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㅇ', selection_start=Some(0), selection_end=Some(0)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='ㅇ'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='이', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='이'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='아', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='아'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='앗', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='앗'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='앟', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='앟'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='았', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='았'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='았', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='았'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:125: on_composition_end: data='았'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=false
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=false, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:88: on_composition_start: data='Some("")'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='ㄷ', selection_start=Some(0), selection_end=Some(0)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='ㄷ'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='디', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='디'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:168: on_keydown: key='Unidentified', code='', is_composing=true, key_code=229
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:103: on_composition_update: data='다', selection_start=Some(0), selection_end=Some(1)
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:61: on_input: input_type="insertCompositionText", is_composing=true, value='다'
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::events] C:\prog\egui\crates\eframe\src\web\events.rs:276: on_keyup: key='Unidentified', code='', is_composing=true
ezchat-ca28cce8b9f513fe.js:1038 [eframe::web::text_agent] C:\prog\egui\crates\eframe\src\web\text_agent.rs:125: on_composition_end: data='다'

@umajho
Copy link
Copy Markdown
Contributor

umajho commented Mar 23, 2026

on_composition_update: data='ㅇ', selection_start=Some(0), selection_end=Some(0)

It appears that Chrome dispatches CompositionEvents before updating the selection state.


on_composition_update: data='ㅅ', selection_start=Some(0), selection_end=Some(0)
…
on_composition_update: data='ㅅ', selection_start=Some(1), selection_end=Some(1)

My interpretation is that Chrome does not expect the text agent's content to be cleared while processing Cheonjiin. To avoid introducing negative values in ranges, it may attempt to recover the text state. (e.g. recovering []ㅅ to 안[]ㅅ, where [] denotes a selection range, which is outdated here.)
(After the second hit of , the state became [않].)


I wonder if we always keep one character at the beginning of the text agent, what would happen in such cases.
Would the transitions look like “x[]안 -> 안[]ㅅ -> [않]”, or “x[]안 -> x안[]ㅅ -> x[않]”? (where x is a random character.)

If the former pattern holds, a possible workaround is to check whether the selection starts at 0 instead of 1.
However, this would only be reliable if the selection state were updated before CompositionEvents are dispatched.


A more robust approach may be to build another IME-handling implementation on top of the EditContext API, as an alternative to the text agent. A proper implementation there should eliminate this kind of inconsistencies for many users(~70%).

For the remaining ~30% of users, we can then try to find workarounds, which I feel more difficult to get right.

@rustbasic
Copy link
Copy Markdown
Contributor Author

It should be handled as follows. In #7914, it is currently behaving like this:
x[안] -> x안[ㅅ] -> x[않] -> x않[ㅇ] -> x않[아]

emilk pushed a commit that referenced this pull request Mar 24, 2026
<!--
Please read the "Making a PR" section of
[`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/main/CONTRIBUTING.md)
before opening a Pull Request!

* Keep your PR:s small and focused.
* The PR title is what ends up in the changelog, so make it descriptive!
* If applicable, add a screenshot or gif.
* If it is a non-trivial addition, consider adding a demo for it to
`egui_demo_lib`, or a new example.
* Do NOT open PR:s from your `master` branch, as that makes it hard for
maintainers to test and add commits to your PR.
* Remember to run `cargo fmt` and `cargo clippy`.
* Open the PR as a draft until you have self-reviewed it and run
`./scripts/check.sh`.
* When you have addressed a PR comment, mark it as resolved.

Please be patient! I will review your PR, but my time is limited!
-->

* Closes #7809
* Closes #7876
* Closes #7908
* Supersedes #7877
* Supersedes #7898
* The author of the PR above replaced it with #7914, which additionally
fixes another IME issue. I believe that fix deserves a separate PR.
* Reverts #4794  
* [x] I have followed the instructions in the PR template

This approach is better than #7898 (#7914) because it correctly handles
all three major IME types (Chinese, Japanese, and Korean) without
requiring a predefined “IME mode”.

## Environments I haved tested this PR in

<details><summary>macOS 15.7.3 (AArch64, Host of other virtual
machines)</summary>

Run command: `cargo run -p egui_demo_app --release`

Tested IMEs:

- builtin Chinese IME (Shuangpin - Simplified)
- builtin Japanese IME (Romaji)
- builtin Korean IME (2-Set)
</details>

<details><summary>Windows 11 25H2 (AArch64, Virtual Machine)</summary>

Build command: `cargo build --release -p egui_demo_app
--target=x86_64-pc-windows-gnu --features=glow --no-default-features`

(I cannot use `wgpu` due to [this
bug](#4381), which prevents
debugging inside the VM. Anyways, the rendering backend should be
irrelevant here.)

Tested IMEs:

- builtin Chinese IME (Shuangpin)
- Sogou IME (Chinese Shuangpin)
- WeType IME (Chinese Shuangpin)
- builtin Japanese IME (Hiragana)
- builtin Korean IME (2 Beolsik)
</details>

<details><summary>Linux [Wayland + IBus] (AArch64, Virtual
Machine)</summary>

Fedora KDE Plasma Desktop 43 [Wayland + IBus 1.5.33-rc2]

(Not working at the moment because of [another
issue](#7485) that will be fixed by
#7983. It is [a complicated
story](#7973 (comment)).
)

> [!NOTE]
>
> IBus is partially broken in this system. The Input Method Selector
refuses to select IBus. As a workaround, I have to open System Settings
-> Virtual Keyboard and select “IBus Wayland” to start an IBus instance
that works in egui.
>
> The funny thing is: the Chinese Intelligent Pinyin IME is broken in
native Apps like System Settings and KWrite, but works correctly in
egui!
>
> <details><summary>Screencast: What</summary>
>
> ![2026-03-13 3 10
11 AM](https://github.com/user-attachments/assets/4001cf12-8089-46f5-9cf4-e41d8f77ee24)
> </details>

Build command: `cross build --release -p egui_demo_app
--target=aarch64-unknown-linux-gnu --features=wayland,wgpu
--no-default-features`

(The Linux toolchain on my mac is somehow broken, so I used `cross`
instead.)

Tested IMEs:

- Chinese Intelligent Pinyin IME (Shuangpin)
- Japanese Anthy IME (Hiragana)
- Korean Hangul IME
</details>

<details><summary>Linux [X11 + Fcitx5] (AArch64, Virtual
Machine)</summary>

Debian 13 [Cinnamon 6.4.10 + X11 + Fcitx5 5.1.2]

Build command: `cross build --release -p egui_demo_app
--target=aarch64-unknown-linux-gnu --features=x11,wgpu
--no-default-features`

Tested IMEs:

- Chinese Shuangpin IME
- Chinese Rime IME with `luna-pinyin`
- Japanese Mozc IME (Hiragana)
- Korean Hangul IME

Unlike macOS and Linux + Wayland, key-release events for keys processed
by the IME are still forwarded to `egui`. These appear to be harmless in
practice.
Unlike on Windows, however, they cannot be filtered reliably because
there are no corresponding key-press events marked as “processed by
IME”.

</details>

---

There are too many possible combinations to test (Operating Systems ×
[Desktop
Environment](https://en.wikipedia.org/wiki/Desktop_environment)s ×
[Windowing System](https://en.wikipedia.org/wiki/Windowing_system)s ×
[IMF](https://wiki.archlinux.org/title/Input_method#Input_method_framework)s
× [IME](https://en.wikipedia.org/wiki/Input_method)s × …), and I only
have access to a limited subset. For example, Google Japanese Input
refused to install on my Windows VM, and some paid Japanese IMEs are not
accessible to me. Therefore, I would appreciate feedback from people
other than me using all kinds of environments.

## Details

There are two possible approaches to removing keyboard events that have
already been processed by an IME:

* Approach 1: Filter out events inside `egui` that appear to have been
received during IME composition.
* Approach 2: Filter out such events in the platform backend
(terminology [borrowed from
imgui](https://github.com/ocornut/imgui/blob/master/docs/BACKENDS.md#using-standard-backends),
e.g. the `egui-winit` crate or the code under `web/` in the `eframe`
crate.).

Both approaches already exist in `egui`:

* #4794 uses the first approach, filtering these events in the
`TextEdit`-related code.
* `eframe` uses the second approach in its web integration. See:
<https://github.com/emilk/egui/blob/14afefa2521d1baaf4fd02105eec2d3727a7ac36/crates/eframe/src/web/events.rs#L173-L176>

Compared to the first approach, the second has a clear advantage: when
events are passed from the platform backends into `egui`, they are
simplified and lose information. In contrast, events in the platform
backends are the original events, which allows them to be handled more
flexibly. This is also why #7898 (#7914), which attempts to address the
issue from within the `egui` crate, struggles to make all IMEs work
correctly at the same time and requires manually selecting an “IME
mode”: the events received by `egui` have already been reduced and
therefore lack necessary information.

A more appropriate solution is to consistently follow the second
approach, explicitly requiring platform backends not to forward events
that have already been processed by the IME to `egui`. This is the
method used in this PR. Specifically, this PR works within the
`egui-winit` crate, where the original `KeyboardInput` events can be
accessed. At least for key press events, these can be used directly to
determine whether the event has already been processed by the IME on
Windows (by checking whether `logical_key` equals
`winit::keyboard::NamedKey::Process`). This makes it straightforward to
ensure that all IMEs work correctly at the same time.

This PR also reverts #4794, which took the first approach. It filters
out some events that merely look like they were received during IME
composition but actually are not. It also messes up the order of those
events along the way.
As a result, it caused several IME-related issues. One of the sections
in the Demonstrations below will illustrate these problems.

## Demonstrations

<details><summary>Changes not included in this PR for displaying Unicode
characters in demonstrations</summary>

Download `unifont-17.0.03.otf` from
<https://unifoundry.com/pub/unifont/unifont-17.0.03/font-builds/>, and
place it at `crates/egui_demo_app/src/unifont-17.0.03.otf`.

In `crates/egui_demo_app/src/wrap_app.rs`, add these lines at the
beginning of `impl WrapApp`'s `pub fn new`:
```rust
        {
            const MAIN_FONT: &'static [u8] = include_bytes!("./unifont-17.0.03.otf");

            let mut fonts = egui::FontDefinitions::default();

            fonts.font_data.insert(
                "main-font".to_owned(),
                std::sync::Arc::new(egui::FontData::from_static(MAIN_FONT)),
            );

            let proportional = fonts
                .families
                .entry(egui::FontFamily::Proportional)
                .or_default();
            proportional.insert(0, "main-font".to_owned());

            cc.egui_ctx.set_fonts(fonts);
        }
```

(I took this from somewhere, but I forgot where it is. Sorry…)
</details>

[GNU Unifont](https://unifoundry.com/unifont/index.html) is licensed
under [OFL-1.1](https://unifoundry.com/OFL-1.1.txt).

### This PR Fixes: Focus on a single-line `TextEdit` is lost after
completing candidate selection with Japanese IME on Windows (#7809)

<details><summary>Screencast: ✅ Japanese IME now behaves correctly while
Korean IME behaves as before</summary>


![7809](https://github.com/user-attachments/assets/6e92f6e6-fed4-46f4-96e1-e0e12dadc360)
</details>

### This PR Fixes: Committing Japanese IME text with <kbd>Enter</kbd>
inserts an unintended newline in multiline `TextEdit` on Windows (#7876)

<details><summary>Screencast: ✅ Japanese IME now behaves correctly while
Korean IME behaves as before</summary>


![7876](https://github.com/user-attachments/assets/03d2cb22-fd0c-45fe-9132-e59fa39bfcf3)
</details>

### This PR Fixes: Backspacing deletes characters during composition in
certain Chinese IMEs (e.g., Sogou) on Windows (#7908)

<details><summary>Screencast: ✅ Sogou IME now behaves
correctly</summary>


![7908](https://github.com/user-attachments/assets/2c63de28-26f0-4387-9c50-dceabfdbe99d)
</details>

### This PR Obsoletes #4794, because `egui` receives only IME events
during composition from now on

On Windows, “incompatible” events are filtered in `egui-winit`, aligning
the behavior with other systems.

<details><summary>Screencasts</summary>

Some Chinese IMEs on Windows:
![2026-03-13 12 25
37 AM](https://github.com/user-attachments/assets/064fd1c7-244b-4053-bd24-c65d768cd943)

The default Japanese IMEs on Windows:
![2026-03-13 12 28
33 AM](https://github.com/user-attachments/assets/f799b0b5-350b-4b05-a769-bcef16255bdb)
</details>

The 2-set Korean IMEs handle arrow keys differently. It will be
discussed in the next section.

### This PR Reverts #4794, because it introduced several bugs

Some of its bugs have already been worked around in the past, but those
workarounds might also be problematic. For example, #4912 is a
workaround for a bug (#4908) introduced by #4794, and that workaround is
in fact the root cause of the macOS backspacing bug I have worked around
with #7810. (The reversion of #4912 is out of the scope of this PR, I
will do that in #7983.)

#### It Caused: Arrow keys are incorrectly blocked during typical Korean
IME composition

When composing Korean text using 2-Set IMEs, users should still be able
to move the cursor with arrow keys regardless if the composition is
committed.

##### Correct behavior

<details><summary>Screencasts</summary>

macOS TextEdit:
![2026-03-12 8 04
15 PM](https://github.com/user-attachments/assets/24383568-f51c-4a74-9251-adfd942cad8f)

Windows Notepad:
![2026-03-12 8 05
08 PM](https://github.com/user-attachments/assets/5a29a5b5-69b8-407b-b1a4-84fdb8f8847d)

With #4794 reverted, `egui` also behaves correctly (tested on Linux +
Wayland, macOS, and Windows):
![2026-03-12 8 03
51 PM](https://github.com/user-attachments/assets/fcb7f25c-1329-4eb1-82f2-1cea33dcca73)
</details>

##### Incorrect behavior caused by #4794

`remove_ime_incompatible_events` removed arrow-key events in such cases.
As a result, the first arrow key press only commits the composition, and
users need to press the arrow key again to move the cursor:

<details><summary>Screencast</summary>

![2026-03-12 8 06
40 PM](https://github.com/user-attachments/assets/6760c6bd-b6ce-44ea-b192-6bd165191c01)
</details>

This is essentially the same issue described here:
#7877 (comment)

#### It Caused: Backspacing leaves the last character in Korean IME
pre-edit text not removed on macOS

<details><summary>Screencasts</summary>

Before this PR:
![2026-03-17 10 48
12 PM](https://github.com/user-attachments/assets/88021e7e-caf6-4aa9-8f73-ecffc63cda06)

After this PR:
![2026-03-17 10 47
23 PM](https://github.com/user-attachments/assets/379cd0db-24e0-4c0e-a5b4-edb37d3e1df7)
</details>

### Korean IMEs also use <kbd>Enter</kbd> to confirm Hanja selections,
and will not work properly in the Korean “IME mode” proposed by #7898
(#7914)

<details><summary>Screencast: Korean IME using <kbd>Enter</kbd> and
<kbd>Space</kbd> for confirmation (IBus Korean Hangul IME)</summary>

The screencast below demonstrates that some Korean IMEs handle Hanja
selection in a way similar to Japanese IMEs: the
<kbd>Up</kbd>/<kbd>Down</kbd> arrow keys are used to navigate
candidates, and <kbd>Enter</kbd> confirms the selected candidate.

![2026-03-13 6 39
17 AM](https://github.com/user-attachments/assets/0b054cc6-2251-4689-95a4-d69a9be36371)
</details>

<details><summary>Screencasts: Another example</summary>

Using the built-in Korean IME on Windows, I type two lines: the first
line in Hangul, and the second line as the same word converted to Hanja.

Correct behavior in Notepad (reference):

![7914-ref](https://github.com/user-attachments/assets/1e9f9315-eb71-497a-b6e2-2b11eb6bbf7f)

Behavior after applying this PR, which matches the Notepad behavior:

![7914-7967](https://github.com/user-attachments/assets/26f12b9f-9354-45b8-b2a8-ede28c34c5b1)

Behavior after applying #7914 with the “IME mode” set to Korean (which
is also the behavior before this PR being applied):

![7914-7914](https://github.com/user-attachments/assets/0f82a019-c491-4b64-a92a-d88d62dfbd84)
On the second line, each time a Hanja character is confirmed, an
unintended newline is inserted. This mirrors the Japanese IME issues
that are supposed to be fixed by setting the “IME mode” to Japanese.
(These Japanese IME issues are fixed in this PR as mentioned before.)

</details>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants