Skip to content

Implement proper visuals for IME composition#8083

Open
umajho wants to merge 14 commits into
emilk:mainfrom
umajho:ime-preedit-visuals
Open

Implement proper visuals for IME composition#8083
umajho wants to merge 14 commits into
emilk:mainfrom
umajho:ime-preedit-visuals

Conversation

@umajho
Copy link
Copy Markdown
Contributor

@umajho umajho commented Apr 8, 2026

  • Closes N/A
  • I have followed the instructions in the PR template

This PR adds visual support for IME composition, including the cursor and conversion segment.
These visuals works (mostly) well on native platforms (egui-winit). On the web (eframe/web), support is limited by browser capabilities: Chromium works well, Firefox shows partial improvement, and Safari remains subpar.

Details

We extend egui::ImeEvent::Preedit(String) to egui::ImeEvent::Preedit { text: String, active_range_chars: Option<std::ops::Range<usize>> }.
The new active_range_chars field enables rendering of:

  • the cursor (when the range is empty), and
  • the conversion segment (when the range is non-empty)

in IME composition.

In egui-winit, we now use the range provided by winit::event::Ime::Preedit instead of ignoring it.

In eframe/web, we derive the range from selectionStart and selectionEnd on the text agent. This mapping is fully accurate only in Chromium, but represents the best available approach for now.

Demonstrations

Chinese IMEs (Shuangpin)

We can see where the cursor is now.

What With this PR Without this PR
macOS builtin
After-macOS-CMN-small.mp4
Before-macOS-CMN-small.mp4
macOS builtin (light)
After-macOS-CMN-light-small.mp4
——
Windows builtin
After-windows-CMN-small.mp4
——
Wayland iBus Intelligent Pinyin
After-wayland-ibus-CMN-IntelligentPinyin-small.mp4
——
Chromium (Chrome) macOS Identical to macOS builtin. ——
Safari macOS We can now differentiate between IME composition and text selection, but we still can't tell where the cursor is. ——
Firefox (Zen) macOS Identical to macOS builtin. ——

Japanese IMEs

We can see where the conversion segment is now.

What With this PR Without this PR
macOS builtin
After-macOS-JPN-small.mp4
Before-macOS-JPN-small.mp4
macOS builtin (light)
After-macOS-JPN-light-small.mp4
——
Windows builtin
After-windows-JPN-small.mp4
——
Wayland iBus Anthy
After-wayland-ibus-JPN-Anthy-small.mp4
——
Chromium (Chrome) macOS Identical to macOS builtin. ——
Safari macOS We can now differentiate between IME composition and text selection, but we still can't tell where the conversion segment is. ——
Firefox (Zen) macOS (Limited improvement.)
After-Firefox-macOS-JPN-small.mp4
——

Korean IMEs

We can clearly tell whether we are in composition (in contrast to selection) now.

What With this PR Without this PR
macOS builtin
After-macOS-KOR-small.mp4
Before-macOS-KOR-small.mp4
macOS builtin (light)
After-macOS-KOR-light-small.mp4
——
Windows builtin (With a workaround for this winit bug applied.)
After-windows-KOR-x-small.mp4
——
Wayland iBus Hangul
After-wayland-ibus-KOR-Hangul-small.mp4
——
Chromium (Chrome) macOS Identical to macOS builtin. ——
Safari macOS Identical to Windows builtin. ——
Firefox (Zen) macOS Identical to macOS builtin. (ignoring the fact that the composition breaks when typing the second Hangul. (This bug predates this PR.)) ——

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2026

Preview available at https://egui-pr-preview.github.io/pr/8083-ime-preedit-visuals
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
Copy link
Copy Markdown
Contributor Author

umajho commented Apr 8, 2026

On Windows, the cursor is visually incorrectly positioned when using the built-in Korean IME:
2026-04-08 9 49 45 PM

This is due to winit reporting incorrect cursor positions. Specifically, the values are returned as Some((0, 0)), but should instead be Some((1, 1)):
2026-04-08 9 49 23 PM

TODO: report this bug to winit.

See: https://issues.chromium.org/issues/41279184

@umajho umajho marked this pull request as ready for review April 9, 2026 10:46
@umajho umajho changed the title Implement visuals for IME preedit Implement proper visuals for IME composition Apr 9, 2026
Preedit {
text: String,
active_range_chars: Option<std::ops::Range<usize>>,
},
Copy link
Copy Markdown
Contributor Author

@umajho umajho Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd actually prefer to define it as:

    Preedit {
        text: String,
        active_range: Option<CCursorRange>,
    },

However, since CCursorRange doesn't implement Eq, this leaves a few options:

  • Use Option<std::ops::Range<usize>>. (the current approach)
  • Remove #[derive(Eq)] from ImeEvent. This is my preferred option, as I don't see a strong need for ImeEvent to implement Eq. That said, I'm unsure whether downstream code might rely on this and thus be affected.
  • Implement Eq for both CCursorRange and CCursor, but it is unclear to me what equality should mean for these types.
  • Implement Eq for ImeEvent manually without requiring Eq on the underlying types, which feels off.

Copy link
Copy Markdown
Contributor Author

@umajho umajho Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another consideration:

If we define active_range as Option<CCursorRange> and later switch the cursor index base from chars to graphemes (as in #62), should active_range also change accordingly (e.g., to Option<GCursorRange>)?

Alternatively, should we keep a more primitive representation (e.g. active_range_chars: Option<std::ops::Range<usize>> or even active_range_bytes: Option<std::ops::Range<usize>>) and handle the conversion within egui?
This would avoid requiring downstream to pull additional crates (e.g. unicode-segmentation), which could introduce duplicated dependencies in binary or inconsistencies with the ones used internally by egui.

@umajho umajho requested a review from lucasmerlin as a code owner April 11, 2026 13:21
@rustbasic
Copy link
Copy Markdown
Contributor

@umajho

In Korean, the text cursor should not be placed before a character; it should be placed after the character.

@umajho
Copy link
Copy Markdown
Contributor Author

umajho commented Apr 29, 2026

In Korean, the text cursor should not be placed before a character; it should be placed after the character.

Yes, I mentioned this earlier in #8083 (comment), where I described it as a bug. As a workaround, I hid the cursor when it is at the beginning.

As I understand it, during Korean composition, the cursor only appears at the end of the composition text. So users should still be able to identify the cursor position by looking at the end of the underline, and since the cursor is hidden, it should not be a distraction.

Did this workaround fail to take effect for you?

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