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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.