bug#81506: [PATCH] Extend w32 window events to expose IME composition string interface
dANiuu zHaO <[email protected]>
| Newsgroups | gmane.emacs.bugs |
|---|---|
| Message-ID | <CAEOE9jdTdN82UD34ZXMiibmH3fEoV_+tnEJcmFEAUBDS6X2s+Q@mail.gmail.com> |
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 KB)
From a0b062b1aa876fee11a3c20cd68055bfbfff4bc6 Mon Sep 17 00:00:00 2001 From: zdn <[email protected]> Date: Thu, 30 Jul 2026 00:42:10 +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 and set preedit text parameters --- lisp/term/w32-win.el | 32 ++++++++++++++++ src/w32fns.c | 87 +++++++++++++++++++++++++++++++++++++++----- src/w32term.c | 39 ++++++++++++++++++++ 3 files changed, 149 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..5b41a33 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,44 @@ 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 one line height gives a better vertical balance. */ + form.ptCurrentPos.y += FRAME_LINE_HEIGHT (f); + /* Setting rcArea to -99999 is to hide the preedit window. + Based on the documentation description, in the WM_IME_SETCONTEXT + event, removing the ISC_SHOWUICOMPOSITIONWINDOW flag from lParam + does work. However, this approach has side effects — once the + preedit window is hidden, dynamically toggling the w32_ime_preedit + variable from Elisp cannot bring the original preedit window back. + As a workaround, I resorted to setting the preedit window + coordinates (to off-screen) instead. + The following was my original code: + + case WM_IME_SETCONTEXT: + if (w32_ime_preedit) + lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW; + goto dflt; + See https://learn.microsoft.com/en-us/windows/win32/intl/wm-ime-setcontext + */ + form.rcArea.left = -99999; + form.rcArea.top = -99999; + } + 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,6 +5126,37 @@ 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); + free (composition_ime_data); + } + else if (lParam & GCS_COMPSTR) + { + if (composition_ime_pt_x < 0) + composition_ime_pt_x = w32_system_caret_x; + + int size; + HIMC context = get_ime_context_fn (hwnd); + + size = get_composition_string_fn (context, GCS_COMPSTR, NULL, 0); + composition_ime_data = malloc (size); + size = get_composition_string_fn (context, GCS_COMPSTR, + composition_ime_data, size); + release_ime_context_fn (hwnd, context); + + signal_user_input (); + my_post_msg (&wmsg, hwnd, msg, + (WPARAM) composition_ime_data, size); + } + + goto dflt; + case WM_IME_ENDCOMPOSITION: ignore_ime_char = 0; goto dflt; diff --git a/src/w32term.c b/src/w32term.c index 728f6ce..8fae547 100644 --- a/src/w32term.c +++ b/src/w32term.c @@ -5387,6 +5387,38 @@ 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 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); + } + + 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 +8424,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