bug#81506: [PATCH] Extend w32 window events to expose IME composition string interface

dANiuu zHaO <[email protected]>
Newsgroups gmane.emacs.bugs
Message-ID <CAEOE9jcM58vHef+04HK67Ex1NLi38adgfG2xoOtFp0CoOJreuA@mail.gmail.com>
Sorry, I forgot to attach the patch file. Here it is.

dANiuu zHaO <[email protected]> 于2026年7月28日周二 10:09写道:

> hello.
>
> This patch adds IME composition string support for the
> MS-Windows (w32) platform.
>
> Add WM_IME_COMPOSITION event processing
>
> Handle the composition string from IME and hide the
> composition string window.  Add an Elisp hook to deliver the
> composition result to Lisp code.
>
> * src/w32fns.c (w32_wnd_proc): Hide composition string window.
> * src/w32term.c (w32_read_socket): Read composition result to Lisp code.
>
> After setting up the hook, the composed string can be drawn using Elisp
> overlays.
> I have created a separate repository to demonstrate the functionality of
> this patch:
>
> https://github.com/zHaOdANiuu/emacs-w32-draw-preedit.patch
>
> I am in the process of completing the FSF copyright assignment.
>
0001-Extend-w32-window-events-to-expose-IME-composition-string-interface.patch (application/octet-stream, 6.1 KB)
From f5465c81c95364a40aeb7dff16669280f08a9dbb Mon Sep 17 00:00:00 2001
From: zdn <[email protected]>
Date: Tue, 28 Jul 2026 08:40:32 +0800
Subject: [PATCH] Extend w32 window events to expose IME composition string
 interface

Add WM_IME_COMPOSITION event processing

Handle the composition string from IME and hide the
composition string window.  Add an Elisp hook to deliver the
composition result to Lisp code.

* src/w32fns.c (w32_wnd_proc): Hide composition string window.
* src/w32term.c (w32_read_socket): Read composition result to Lisp code.
---
 src/w32fns.c  | 83 +++++++++++++++++++++++++++++++++++++++++++++------
 src/w32term.c | 25 ++++++++++++++++
 2 files changed, 99 insertions(+), 9 deletions(-)

diff --git a/src/w32fns.c b/src/w32fns.c
index 726fe52..6774a26 100644
--- a/src/w32fns.c
+++ b/src/w32fns.c
@@ -271,6 +271,13 @@ #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.
+   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];
+
 /* W95 mousewheel handler */
 extern unsigned int msh_mousewheel;
 unsigned int msh_mousewheel = 0;
@@ -5058,15 +5065,41 @@ 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)
+	    {
+	      /* 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 -9999 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 = -9999;
+	    }
+	  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 +5121,38 @@ 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)
+	{
+	  my_post_msg (&wmsg, hwnd, WM_IME_COMPOSITION, (WPARAM) "", 0);
+	}
+      else if (lParam & GCS_COMPSTR)
+	{
+	  int size, wlen;
+	  wchar_t *wbuf = alloca (sizeof(composition_ime_buf));
+	  HIMC context = get_ime_context_fn (hwnd);
+
+	  memset(composition_ime_buf, 0, sizeof(composition_ime_buf));
+	  size = get_composition_string_fn (context, GCS_COMPSTR,
+					    wbuf, sizeof (composition_ime_buf));
+	  release_ime_context_fn (hwnd, context);
+
+	  wlen = WideCharToMultiByte (CP_UTF8, 0,
+				      wbuf,
+				      size / sizeof (wchar_t),
+				      composition_ime_buf,
+				      sizeof (composition_ime_buf),
+				      NULL, NULL);
+	  signal_user_input ();
+	  my_post_msg (&wmsg, hwnd, WM_IME_COMPOSITION,
+		       (WPARAM) &composition_ime_buf, wlen);
+	}
+
+      goto dflt;
+
     case WM_IME_ENDCOMPOSITION:
       ignore_ime_char = 0;
       goto dflt;
diff --git a/src/w32term.c b/src/w32term.c
index 728f6ce..fd9a3c0 100644
--- a/src/w32term.c
+++ b/src/w32term.c
@@ -5387,6 +5387,15 @@ w32_read_socket (struct terminal *terminal,
 	    }
 	  break;
 
+	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);
+	  }
+	  break;
+
         case WM_APPCOMMAND:
 	  f = w32_window_to_frame (dpyinfo, msg.msg.hwnd);
 
@@ -8392,6 +8401,22 @@ 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;
+
+  DEFVAR_LISP ("w32-ime-composition-hook",
+	       Vw32_ime_composition_hook,
+	       doc: /* Hook run when the IME composition string changes.
+This hook is called during IME composition, before the composed text
+is finalized and inserted into the buffer.  The hook function receives
+one argument, the current composition string (as a multibyte string).  */);
+  Vw32_ime_composition_hook = Qnil;
+  DEFSYM (Qw32_ime_composition_hook, "w32-ime-composition-hook");
+
   /* 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.