Win32: Implement text input focus with IMM

When explicit text input focus is inactive, keep GLFW from processing its
own WM_IME_* composition, preedit, candidate and IME status paths while
preserving normal key input.

On Win32, gating GLFW message handling is not enough to prevent native IMM
candidate UI from appearing. Temporarily associate a NULL HIMC with the
window while explicit text input focus is out, then restore the saved HIMC
when focus returns.

Cancel and clear active preedit/candidate state as focus-out cleanup. After
restoring the HIMC, reapply the preedit cursor rectangle so candidate UI
uses the latest location.
This commit is contained in:
Takuro Ashie 2026-06-21 16:29:05 +09:00
parent 44ec6b2467
commit a8819c7924
3 changed files with 99 additions and 8 deletions

View file

@ -172,6 +172,8 @@ static GLFWbool loadLibraries(void)
_glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetCompositionStringW");
_glfw.win32.imm32.ImmGetContext_ = (PFN_ImmGetContext)
_glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetContext");
_glfw.win32.imm32.ImmAssociateContext_ = (PFN_ImmAssociateContext)
_glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmAssociateContext");
_glfw.win32.imm32.ImmGetConversionStatus_ = (PFN_ImmGetConversionStatus)
_glfwPlatformGetModuleSymbol(_glfw.win32.imm32.instance, "ImmGetConversionStatus");
_glfw.win32.imm32.ImmGetDescriptionW_ = (PFN_ImmGetDescriptionW)

View file

@ -263,6 +263,7 @@ typedef LONG (WINAPI * PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*,ULONG,ULONGLO
typedef DWORD (WINAPI * PFN_ImmGetCandidateListW)(HIMC,DWORD,LPCANDIDATELIST,DWORD);
typedef LONG (WINAPI * PFN_ImmGetCompositionStringW)(HIMC,DWORD,LPVOID,DWORD);
typedef HIMC (WINAPI * PFN_ImmGetContext)(HWND);
typedef HIMC (WINAPI * PFN_ImmAssociateContext)(HWND,HIMC);
typedef BOOL (WINAPI * PFN_ImmGetConversionStatus)(HIMC,LPDWORD,LPDWORD);
typedef UINT (WINAPI * PFN_ImmGetDescriptionW)(HKL,LPWSTR,UINT);
typedef BOOL (WINAPI * PFN_ImmGetOpenStatus)(HIMC);
@ -274,6 +275,7 @@ typedef BOOL (WINAPI * PFN_ImmSetOpenStatus)(HIMC,BOOL);
#define ImmGetCandidateListW _glfw.win32.imm32.ImmGetCandidateListW_
#define ImmGetCompositionStringW _glfw.win32.imm32.ImmGetCompositionStringW_
#define ImmGetContext _glfw.win32.imm32.ImmGetContext_
#define ImmAssociateContext _glfw.win32.imm32.ImmAssociateContext_
#define ImmGetConversionStatus _glfw.win32.imm32.ImmGetConversionStatus_
#define ImmGetDescriptionW _glfw.win32.imm32.ImmGetDescriptionW_
#define ImmGetOpenStatus _glfw.win32.imm32.ImmGetOpenStatus_
@ -380,6 +382,7 @@ typedef struct _GLFWlibraryWGL
typedef struct _GLFWwindowWin32
{
HWND handle;
HIMC textInputContext;
HICON bigIcon;
HICON smallIcon;
@ -473,6 +476,7 @@ typedef struct _GLFWlibraryWin32
PFN_ImmGetCandidateListW ImmGetCandidateListW_;
PFN_ImmGetCompositionStringW ImmGetCompositionStringW_;
PFN_ImmGetContext ImmGetContext_;
PFN_ImmAssociateContext ImmAssociateContext_;
PFN_ImmGetConversionStatus ImmGetConversionStatus_;
PFN_ImmGetDescriptionW ImmGetDescriptionW_;
PFN_ImmGetOpenStatus ImmGetOpenStatus_;

View file

@ -834,6 +834,11 @@ static void clearImmPreedit(_GLFWwindow* window)
_glfwInputPreedit(window);
}
static GLFWbool textInputFocusDisabled(_GLFWwindow* window)
{
return window->textInputFocusInitialized && !window->textInputFocus;
}
// Commit the result texts of Imm32 to character-callback
//
static GLFWbool commitImmResultStr(_GLFWwindow* window)
@ -904,6 +909,9 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
{
case WM_IME_SETCONTEXT:
{
if (textInputFocusDisabled(window))
break;
// To draw preedit text by an application side
if (lParam & ISC_SHOWUICOMPOSITIONWINDOW)
lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW;
@ -1146,6 +1154,9 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
case WM_IME_COMPOSITION:
{
if (textInputFocusDisabled(window))
return 0;
if (lParam & (GCS_RESULTSTR | GCS_COMPSTR))
{
if (lParam & GCS_RESULTSTR)
@ -1159,6 +1170,9 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
case WM_IME_ENDCOMPOSITION:
{
if (textInputFocusDisabled(window))
return 0;
clearImmPreedit(window);
// Usually clearing candidates in IMN_CLOSECANDIDATE is sufficient.
// However, some IME need it here, e.g. Google Japanese Input.
@ -1168,6 +1182,9 @@ static LRESULT CALLBACK windowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
case WM_IME_NOTIFY:
{
if (textInputFocusDisabled(window))
return 0;
switch (wParam)
{
case IMN_SETOPENSTATUS:
@ -1955,6 +1972,9 @@ void _glfwDestroyWindowWin32(_GLFWwindow* window)
if (window->win32.handle)
{
if (window->win32.textInputContext)
ImmAssociateContext(window->win32.handle, window->win32.textInputContext);
RemovePropW(window->win32.handle, L"GLFW");
DestroyWindow(window->win32.handle);
window->win32.handle = NULL;
@ -2857,6 +2877,9 @@ void _glfwUpdatePreeditCursorRectangleWin32(_GLFWwindow* window)
int h = preedit->cursorHeight;
COMPOSITIONFORM areaRect = { CFS_RECT, { x, y }, { x, y, x + w, y + h } };
if (!hIMC)
return;
ImmSetCompositionWindow(hIMC, &areaRect);
CANDIDATEFORM excludeRect = { 0, CFS_EXCLUDE, { x, y }, { x, y, x + w, y + h } };
@ -2869,29 +2892,91 @@ void _glfwResetPreeditTextWin32(_GLFWwindow* window)
{
HWND hWnd = window->win32.handle;
HIMC hIMC = ImmGetContext(hWnd);
ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0);
ImmReleaseContext(hWnd, hIMC);
GLFWbool releaseContext = GLFW_TRUE;
if (!hIMC && window->win32.textInputContext)
{
hIMC = window->win32.textInputContext;
releaseContext = GLFW_FALSE;
}
if (hIMC)
{
ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL, 0);
if (releaseContext)
ImmReleaseContext(hWnd, hIMC);
}
clearImmPreedit(window);
clearImmCandidate(window);
}
void _glfwSetTextInputFocusWin32(_GLFWwindow* window, GLFWbool focused)
{
// TODO: Add safe IMM/TSF text input focus plumbing without changing IME status.
if (focused)
{
if (window->win32.textInputContext)
{
ImmAssociateContext(window->win32.handle,
window->win32.textInputContext);
window->win32.textInputContext = NULL;
_glfwUpdatePreeditCursorRectangleWin32(window);
}
}
else
{
_glfwResetPreeditTextWin32(window);
if (!window->win32.textInputContext)
{
window->win32.textInputContext =
ImmAssociateContext(window->win32.handle, NULL);
}
}
}
void _glfwSetIMEStatusWin32(_GLFWwindow* window, int active)
{
HWND hWnd = window->win32.handle;
HIMC hIMC = ImmGetContext(hWnd);
HIMC hIMC = window->win32.textInputContext;
GLFWbool releaseContext = GLFW_FALSE;
if (!hIMC)
{
hIMC = ImmGetContext(hWnd);
releaseContext = GLFW_TRUE;
}
if (!hIMC)
return;
ImmSetOpenStatus(hIMC, active ? TRUE : FALSE);
ImmReleaseContext(hWnd, hIMC);
if (releaseContext)
ImmReleaseContext(hWnd, hIMC);
}
int _glfwGetIMEStatusWin32(_GLFWwindow* window)
{
HWND hWnd = window->win32.handle;
HIMC hIMC = ImmGetContext(hWnd);
BOOL result = ImmGetOpenStatus(hIMC);
ImmReleaseContext(hWnd, hIMC);
HIMC hIMC = window->win32.textInputContext;
GLFWbool releaseContext = GLFW_FALSE;
BOOL result;
if (!hIMC)
{
hIMC = ImmGetContext(hWnd);
releaseContext = GLFW_TRUE;
}
if (!hIMC)
return GLFW_FALSE;
result = ImmGetOpenStatus(hIMC);
if (releaseContext)
ImmReleaseContext(hWnd, hIMC);
return result ? GLFW_TRUE : GLFW_FALSE;
}