bug#81506: [PATCH] Extend w32 window events to expose IME composition string interface
dANiuu zHaO <[email protected]> Fri, 31 Jul 2026 23:32:09 +0800
| Newsgroups | gmane.emacs.bugs |
|---|---|
| Message-ID | <CAEOE9jcB+Cn=eKZqeQSDvpqsK0KhozqaCwxMS8JBaz2w7izkJw@mail.gmail.com> |
[PATCH 3] Implement preedit text rendering for the IME window on w32 > This is the second patch. > I fixed the static buffer issue from yesterday by switching to on-demand allocation.> > For w32term.c, I removed the direct execution of Lisp code and instead communicate by writing to inev's args, following the preedit handling approach in xterm.c. > I kept the w32-ime-preedit flag to toggle the default IME window. > The hook-based approach has been completely removed. Everything now works through event reading. This is a version based on the previous patch. This version adds the following features: - When composing CJK strings, the IME window no longer jitters along with the cursor, keeping the behavior consistent with the native Windows input method behavior. - When composing CJK strings, while Emacs renders the preedit text, the cursor can respond to arrow keys — specifically <left> and <right> behavior. If you have any questions or concerns about the code, please feel free to contact me. I have set up a dedicated example repository to document the development of this patch from scratch: https://github.com/zHaOdANiuu/emacs-w32-draw-preedit.patch Later on, I will attempt to implement the font size feature described in https://debbugs.gnu.org/cgi/bugreport.cgi?bug=68339 , in order to improve the overall Windows input method experience in Emacs. dANiuu zHaO <[email protected]> 于2026年7月30日周四 00:57写道: > This is the second patch. > I fixed the static buffer issue from yesterday by switching to on-demand > allocation. > For w32term.c, I removed the direct execution of Lisp code and instead > communicate by writing to inev's args, following the preedit handling > approach in xterm.c. > I kept the w32-ime-preedit flag to toggle the default IME window. > The hook-based approach has been completely removed. Everything now works > through event reading. > > dANiuu zHaO <[email protected]> 于2026年7月29日周三 22:33写道: > >> For the IME window font size setting, I will try to implement it, and >> once completed I will separate it into a standalone patch to facilitate >> review. >> However, what I currently prefer is having Emacs handle the rendering of >> preedit text, because I prefer the preedit style more. >> That said, I also understand that this feature involves quite a lot of >> changes and will require a lengthy review and testing process. In >> comparison, the font-size approach would be much easier to review and test. >> >> I revised the patch based on yesterday's email feedback, including string >> allocation and other items. I made quite a few improvements to the code. I >> expect to send the second patch in a few hours. >> >> > I see that you added a hook, but I think we should discuss the use >> > cases for such a hook. Also, we generally avoid installing features >> > on Windows that have no equivalents on GNU/Linux, so does Emacs have >> > anything similar on X? >> >> I took a look at other files under the src directory today and found that >> xterm.c, xfns.c, and their corresponding Lisp files all have preedit >> handling. I removed the hook approach and switched to an event-reading >> approach instead. However, I still kept a Lisp flag, intending to implement >> the ImmSetCompositionFont approach mentioned above. That way there can be >> two sets of IME display control, allowing users to choose between the >> preedit style and the default IME window. >> >> * .\src\xfns.c (xic_preedit_caret_callback) >> * .\src\xterm.c (x_maybe_clear_preedit) >> * .\lisp\term\x-win.el (x-preedit-text) >> >> Based on the code in these files, I learned about `PREEDIT_TEXT_EVENT`. >> I'm glad — I think I now know how to write this properly. >> >> Eli Zaretskii <[email protected]> 于2026年7月29日周三 20:11写道: >> >>> [Please always use Reply to All when replying, to keep the bug tracker >>> CC'ed.] >>> >>> > From: dANiuu zHaO <[email protected]> >>> > Date: Tue, 28 Jul 2026 23:56:48 +0800 >>> > >>> > Thanks for your review. Since this is my first time contributing code, >>> there are indeed many areas where I can >>> > improve. >>> > >>> > Let me first address the question about the purpose of the changes. >>> The relevant bug report is: >>> > >>> > https://debbugs.gnu.org/cgi/bugreport.cgi?bug=68339 >>> > >>> > Emacs on Windows already supports the IME window, but the IME preedit >>> window does not scale with the font >>> > size in the buffer. This report describes exactly this issue — when >>> using Microsoft Pinyin IME to type Chinese >>> > on Windows, the font size in the preedit window is too small to be >>> legible. I also don't have a good approach >>> > for using ImmSetCompositionFont to address this. So instead, I >>> extended the WM_IME_COMPOSITION event >>> > to obtain the composition string from CJK input methods. My idea is >>> that after obtaining these strings, I can >>> > pass them to Elisp to render using overlays, rather than relying on >>> the IME's own window to draw them. This is >>> > a new feature. >>> > >>> > This hook reads the composition string from the WM_IME_COMPOSITION >>> event, and its sole purpose is this >>> > use case. >>> >>> If this is for making the font of the IME window more like the size of >>> the default font, then why not use ImmSetCompositionFont, as suggested >>> by bug#68339? I know that I said there it was not possible, but I >>> think I was wrong: using ImmGetCompositionFont, we could retrieve the >>> LOGFONT structure for the font the IME window uses, and then modify >>> just one member of the structure, lfHeight, by setting it to the >>> negative of FRAME_FONT (f)->pixel_size, and call >>> ImmSetCompositionFont. AFAIU, this should be done when we receive the >>> WM_IME_STARTCOMPOSITION message, with the same context we use for >>> calling ImmSetCompositionWindow. Can you try that? >>> >>> > Regarding whether there is an equivalent implementation on GNU/Linux >>> and X — I honestly don't know. I do >>> > most of my work on Windows, so I'm not familiar with that. I >>> apologize for this. >>> >>> If we use this for setting the font of the IME window, then we are >>> okay, because we already have a similar code in xfns.c for X. >>> However, providing a hook is much more general than this, so this is >>> one more reason not to go the hook way. >>> >>> > As for the discussion from a few days ago about supporting IME in >>> terminal (-nw) sessions — that is a difficult >>> > challenge. I think it heavily depends on whether the terminal itself >>> handles IME input properly. I haven't even >>> > looked at Microsoft's documentation on this topic yet. So I believe >>> it is unrelated to our previous discussion. >>> >>> Understood. >>> >>> > Regarding the code comments and style — those oversights were my >>> mistake, and I will fix them in the next >>> > patch. >>> > >>> > The static buffer size limit is indeed too small. I will switch to >>> using MAX_ALLOCA or a similar approach. >>> > >>> > > +/* Fixed buffer for receiving IME composition (preedit) string. >>> > > + IME composition strings are typically short, so 64 characters >>> > > + is sufficient for most cases. The buffer is sized to 2 * 64 >>> > > + bytes because each wide character (wchar_t) occupies 2 bytes >>> > > + under UTF-16 encoding on Windows. */ >>> > > +static char composition_ime_buf[2 * 64]; >>> > >>> > For the wchar_t buffer allocation, multiplying by 4 when converting to >>> UTF-8 would indeed be better. The IME >>> > composition strings produced by the input method I personally use do >>> not exceed 2 bytes when converted to >>> > UTF-8, so I made that assumption and overlooked the fact that other >>> CJK input methods may produce longer >>> > strings. >>> > >>> > > + wlen = WideCharToMultiByte (CP_UTF8, 0, >>> > > + wbuf, >>> > > + size / sizeof (wchar_t), >>> > > + composition_ime_buf, >>> > > + sizeof (composition_ime_buf), >>> > > + NULL, NULL); >>> > >>> > Regarding calling Lisp inside read_socket_hook — I also feel that >>> calling Lisp there is somewhat inappropriate; >>> > it is a bit slow. As for safety, I haven't encountered issues in my >>> own local testing, but more testing is needed to >>> > verify. So I would like to know: what is the recommended path within >>> the project for notifying Lisp with data >>> > obtained from window events? I will follow whatever the recommended >>> approach is. >>> > >>> > > + case WM_IME_COMPOSITION: >>> > > + { >>> > > + int len = (int) msg.msg.lParam; >>> > > + char *buf = (char *) msg.msg.wParam; >>> > > + Lisp_Object str = make_string_from_utf8 (buf, len); >>> > > + CALLN (Frun_hook_with_args, Qw32_ime_composition_hook, >>> str); >>> > >>> > Regarding the review of w32-ime-preedit and w32-ime-composition-hook — >>> you are right that w32-ime-preedit >>> > is not needed. We can simply check whether w32-ime-composition-hook >>> is nil. I didn't think of this at all when >>> > writing it. I agree this is absolutely the right approach. >>> > >>> > Finally, thank you for your review. I plan to send a second patch in >>> the next few days. I would like to get this >>> > feature into Emacs — it will allow users on Windows to see the IME >>> preedit window clearly without needing to >>> > install a third-party input method package. >>> >>> Thanks. >>> >>
0001-Implement-preedit-text-rendering-for-the-IME-window-.patch
(application/octet-stream, 8.4 KB)
From 7806a8919b7cce74c7dbeb65583767a949cc18f3 Mon Sep 17 00:00:00 2001 From: zdn <[email protected]> Date: Thu, 30 Jul 2026 19:06:09 +0800 Subject: [PATCH] Implement preedit text rendering for the IME window on w32 * lisp/term/w32-win.el (w32-clear-preedit-text, w32-preedit-text): Draw w32 IME preedit text. * src/w32fns.c (w32_wnd_proc): Extend the WM_IME_COMPOSITION event and handle edge cases in WM_IME_STARTCOMPOSITION * src/w32term.c (w32_read_socket): Handle WM_IME_COMPOSITION message --- lisp/term/w32-win.el | 32 ++++++++++++++++ src/w32fns.c | 87 +++++++++++++++++++++++++++++++++++++++----- src/w32term.c | 46 +++++++++++++++++++++++ 3 files changed, 156 insertions(+), 9 deletions(-) diff --git a/lisp/term/w32-win.el b/lisp/term/w32-win.el index 385f488..c6d9e12 100644 --- a/lisp/term/w32-win.el +++ b/lisp/term/w32-win.el @@ -621,6 +621,38 @@ w32-find-non-USB-fonts (clear-font-cache) (and val (setq w32-non-USB-fonts val)))) +;; The preedit rendering code below is adapted from x-win.el, +;; with the variable and function prefixes changed to w32 and a few minor modifications. +(defvar w32-preedit-overlay nil + "The overlay currently used to display preedit text from a compose sequence.") + +(defun w32-clear-preedit-text () + "Clear the pre-edit overlay and remove itself from `pre-command-hook'. +This function should be installed in `pre-command-hook' whenever +preedit text is displayed." + (when w32-preedit-overlay + (delete-overlay w32-preedit-overlay) + (setq w32-preedit-overlay nil)) + (remove-hook 'pre-command-hook #'w32-clear-preedit-text)) + +(defun w32-preedit-text (event) + "Display preedit text from a compose sequence in EVENT. +EVENT is a preedit-text event." + (interactive "e") + (when w32-ime-preedit + (when w32-preedit-overlay + (delete-overlay w32-preedit-overlay) + (setq w32-preedit-overlay nil) + (remove-hook 'pre-command-hook #'w32-clear-preedit-text)) + (when (nth 1 event) + (let ((string (propertize (nth 1 event) 'face '(:underline t)))) + (setq w32-preedit-overlay (make-overlay (point) (point))) + (add-hook 'pre-command-hook #'w32-clear-preedit-text) + (overlay-put w32-preedit-overlay 'window (selected-window)) + (overlay-put w32-preedit-overlay 'before-string string))))) + +(define-key special-event-map [preedit-text] 'w32-preedit-text) + (provide 'w32-win) (provide 'term/w32-win) diff --git a/src/w32fns.c b/src/w32fns.c index 726fe52..fd2c5eb 100644 --- a/src/w32fns.c +++ b/src/w32fns.c @@ -271,6 +271,15 @@ #define CCHDEVICENAME 32 /* Flag to selectively ignore WM_IME_CHAR messages. */ static int ignore_ime_char = 0; +/* Fixed buffer for receiving IME composition (preedit) string. */ +static wchar_t *composition_ime_data; + +/* Used to preserve the position of the IME window during the WM_IME_COMPOSITION event, + preventing other events from updating the w32_system_caret_x variable and + causing the IME window's x position to jitter, + in order to remain consistent with Windows' default behavior. */ +static int composition_ime_pt_x = -1; + /* W95 mousewheel handler */ extern unsigned int msh_mousewheel; unsigned int msh_mousewheel = 0; @@ -5058,15 +5067,35 @@ w32_wnd_proc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) form.ptCurrentPos.x = w32_system_caret_x; form.ptCurrentPos.y = w32_system_caret_y; - form.rcArea.left = WINDOW_TEXT_TO_FRAME_PIXEL_X (w, 0); - form.rcArea.top = (WINDOW_TOP_EDGE_Y (w) - + w32_system_caret_hdr_height); - form.rcArea.right = (WINDOW_BOX_RIGHT_EDGE_X (w) - - WINDOW_RIGHT_MARGIN_WIDTH (w) - - WINDOW_RIGHT_FRINGE_WIDTH (w)); - form.rcArea.bottom = (WINDOW_BOTTOM_EDGE_Y (w) - - WINDOW_BOTTOM_DIVIDER_WIDTH (w) - - w32_system_caret_mode_height); + if (w32_ime_preedit) + { + if (composition_ime_pt_x > 0) + form.ptCurrentPos.x = composition_ime_pt_x; + /* After hiding the preedit window, the original position is + a bit too high. Subtracting half the font pixel size gives + better vertical centering. The extra 2 pixels prevent the + window from touching the edge. */ + form.ptCurrentPos.y -= -FRAME_FONT (f)->pixel_size / 2 - 2; + /* Set rcArea left and top to negative values to hide the preedit window. + Removing the ISC_SHOWUICOMPOSITIONWINDOW flag from lParam in + WM_IME_SETCONTEXT has no effect. + See: https://learn.microsoft.com/en-us/windows/win32/intl/wm-ime-setcontext + So we move the constraint area offscreen instead. */ + form.rcArea.left = -LONG_MAX; + form.rcArea.top = -LONG_MAX; + } + else + { + form.rcArea.left = WINDOW_TEXT_TO_FRAME_PIXEL_X (w, 0); + form.rcArea.top = (WINDOW_TOP_EDGE_Y (w) + + w32_system_caret_hdr_height); + form.rcArea.right = (WINDOW_BOX_RIGHT_EDGE_X (w) + - WINDOW_RIGHT_MARGIN_WIDTH (w) + - WINDOW_RIGHT_FRINGE_WIDTH (w)); + form.rcArea.bottom = (WINDOW_BOTTOM_EDGE_Y (w) + - WINDOW_BOTTOM_DIVIDER_WIDTH (w) + - w32_system_caret_mode_height); + } /* Punt if the window was deleted behind our back. */ if (!BUFFERP (w->contents)) @@ -5088,8 +5117,48 @@ w32_wnd_proc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) supported by the input method... */ goto dflt; + case WM_IME_COMPOSITION: + if (!w32_ime_preedit) + goto dflt; + + if (lParam & GCS_RESULTSTR) + { + composition_ime_pt_x = -1; + my_post_msg (&wmsg, hwnd, msg, (WPARAM) "", 0); + } + else if (lParam & (GCS_COMPSTR | GCS_CURSORPOS)) + { + int size = 0; + int cursor_pos = 0; + HIMC context = get_ime_context_fn (hwnd); + + if (composition_ime_pt_x < 0) + composition_ime_pt_x = w32_system_caret_x; + + free (composition_ime_data); + composition_ime_data = NULL; + + size = get_composition_string_fn (context, GCS_COMPSTR, NULL, 0); + composition_ime_data = malloc (size + sizeof (cursor_pos)); + size = get_composition_string_fn (context, GCS_COMPSTR, + composition_ime_data, size); + cursor_pos = get_composition_string_fn (context, GCS_CURSORPOS, + NULL, 0); + /* offset end, write cursor position. */ + memcpy ((char *) composition_ime_data + size, + &cursor_pos, sizeof (cursor_pos)); + release_ime_context_fn (hwnd, context); + + my_post_msg (&wmsg, hwnd, msg, + (WPARAM) composition_ime_data, size); + break; + } + + goto dflt; + case WM_IME_ENDCOMPOSITION: ignore_ime_char = 0; + composition_ime_pt_x = -1; goto dflt; /* Simulate middle mouse button events when left and right buttons diff --git a/src/w32term.c b/src/w32term.c index 728f6ce..58e915b 100644 --- a/src/w32term.c +++ b/src/w32term.c @@ -5387,6 +5387,45 @@ w32_read_socket (struct terminal *terminal, } break; + case WM_IME_COMPOSITION: + { + f = w32_window_to_frame (dpyinfo, msg.msg.hwnd); + Lisp_Object str = Qnil; + int size = (int) msg.msg.lParam; + + if (size > 0) + { + int cursor_pos; + int utf8_len; + char *utf8_buf; + wchar_t *wbuf = (wchar_t *) msg.msg.wParam; + utf8_len = WideCharToMultiByte (CP_UTF8, 0, + wbuf, size / sizeof(wchar_t), + NULL, 0, + NULL, NULL); + utf8_buf = alloca (utf8_len); + utf8_len = WideCharToMultiByte(CP_UTF8, 0, + wbuf, size / sizeof(wchar_t), + utf8_buf, utf8_len, + NULL, NULL); + str = make_string_from_utf8 (utf8_buf, utf8_len); + + /* offaet end, read cursor position. */ + memcpy (&cursor_pos, (char *) wbuf + size, sizeof (cursor_pos)); + /* see xic_preedit_draw_callback function. */ + Fput_text_property (make_fixnum (min (SCHARS (str), max (0, cursor_pos))), + make_fixnum (min (SCHARS (str), max (0, cursor_pos) + 1)), + Qcursor, Qt, str); + } + + inev.kind = PREEDIT_TEXT_EVENT; + inev.arg = str; + inev.modifiers = msg.dwModifiers; + XSETFRAME (inev.frame_or_window, f); + inev.timestamp = msg.msg.time; + } + break; + case WM_APPCOMMAND: f = w32_window_to_frame (dpyinfo, msg.msg.hwnd); @@ -8392,6 +8431,13 @@ syms_of_w32term (void) API. */); w32_add_wrapped_menu_bar_lines = 1; + DEFVAR_BOOL ("w32-ime-preedit", + w32_ime_preedit, + doc: /* Non-nil means report IME preedit strings to +`w32-ime-composition-hook'. When nil, the IME preedit is not +reported and the IME works as normal. */); + w32_ime_preedit = 0; + /* Tell Emacs about this window system. */ Fprovide (Qw32, Qnil); } -- 2.54.0