From e9aa24f1d494f95426fa217b853c0b96b2c013f1 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:24:32 +0900 Subject: [PATCH 1/7] Add text input focus API plumbing Introduce glfwSetTextInputFocus(window, focused), internal state and a platform hook for explicit application text input focus. Track whether the application has explicitly called the API with textInputFocusInitialized, so existing applications keep legacy platform behavior until they opt into explicit text input focus. This follows the text input focus direction from the IME support discussions in clear-code/glfw#5 and clear-code/glfw#7, without changing platform behavior yet. --- docs/input.md | 51 ++++++++++++++++++++++++++++++++++++++++++++ include/GLFW/glfw3.h | 37 ++++++++++++++++++++++++++++++++ src/cocoa_init.m | 1 + src/cocoa_platform.h | 1 + src/cocoa_window.m | 5 +++++ src/input.c | 13 +++++++++++ src/internal.h | 7 ++++++ src/null_init.c | 1 + src/null_platform.h | 1 + src/null_window.c | 4 ++++ src/win32_init.c | 1 + src/win32_platform.h | 1 + src/win32_window.c | 5 +++++ src/wl_init.c | 1 + src/wl_platform.h | 1 + src/wl_window.c | 5 +++++ src/x11_init.c | 1 + src/x11_platform.h | 1 + src/x11_window.c | 4 ++++ 19 files changed, 141 insertions(+) diff --git a/docs/input.md b/docs/input.md index a502ffa9f6..8994e2c39c 100644 --- a/docs/input.md +++ b/docs/input.md @@ -276,6 +276,47 @@ In this case, the preedit callback also works on X11. However, on-the-spot styl X11 is unstable, so it is not recommended. +@subsection input_text_focus Text input focus + +Text input focus describes whether the application is currently in a text input +context. Examples include a chat box, text field, search box, rename dialog or +editor. This is distinct from native window focus. + +This is useful for applications that render their own user interface inside a +single native window, such as games and browsers. In these applications, the +native window may remain focused while the application switches between +gameplay, menus, chat input, search fields, editors and other UI elements. The +application is the only component that reliably knows when text input is +expected. + +Use @ref glfwSetTextInputFocus to tell GLFW when the application enters or +leaves a text input context: + +@code +glfwSetTextInputFocus(window, GLFW_TRUE); // Text field became active +glfwSetTextInputFocus(window, GLFW_FALSE); // Text field lost focus +@endcode + +This function does not turn the IME on or off. It expresses whether GLFW should +route text input through the platform text input or IME path for this window. +It does not request an input language change, force a specific IME state or +force a specific input source. + +For compatibility, GLFW preserves the previous platform text input behavior for +applications that never call @ref glfwSetTextInputFocus. Once an application +calls it for a window, that window enters explicit text input focus management. +The application is then responsible for calling it with `GLFW_TRUE` when text +input begins and with `GLFW_FALSE` when text input ends. + +Applications that opt into explicit text input focus management should set the +initial state explicitly, usually to `GLFW_FALSE`, after window creation. They +should then set it to `GLFW_TRUE` only while a text input widget is active. + +Individual platforms map this abstraction to their native text input +mechanisms. Some platforms may also cancel or clear active preedit text when +text input focus is set to `GLFW_FALSE`. + + @subsection input_preedit Preedit input When inputting text with IME, the text is temporarily inputted, then conversion @@ -407,6 +448,16 @@ glfwSetInputMode(window, GLFW_IME, GLFW_TRUE); glfwSetInputMode(window, GLFW_IME, GLFW_FALSE); @endcode +This is related to but distinct from @ref glfwSetTextInputFocus. Text input +focus describes application intent: the application has entered or left a text +input context. `GLFW_IME` controls platform-specific IME state. Applications +should normally prefer @ref glfwSetTextInputFocus unless they specifically need +platform-dependent IME state control. + +As a rule of thumb, if you think you need to enable or disable IME because a +chat box, text field, search field, rename dialog or editor gained or lost +focus, you probably want text input focus instead. + You can use the following function to clear the current preedit. @code diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index cb75ad99cd..06ba3dde4b 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -5310,6 +5310,43 @@ GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int */ GLFWAPI void glfwResetPreeditText(GLFWwindow* window); +/*! @brief Sets whether the application is in a text input context. + * + * This function tells GLFW whether the specified window is currently handling + * application text input, such as a chat box, text field, search box, rename + * dialog or editor. Text input focus is separate from native window focus. + * + * Pass `GLFW_TRUE` when the application enters a text input context and + * `GLFW_FALSE` when it leaves that context. This does not turn the IME on or + * off, switch input languages or force a specific platform input source. + * + * For compatibility, applications that never call this function keep the same + * platform text input behavior as before this API was introduced. Once this + * function is called for a window, the application is responsible for + * notifying GLFW when text input begins and ends. + * + * This function is related to but distinct from `GLFW_IME`. Applications + * should normally prefer this function unless they specifically need + * platform-dependent IME state control. + * + * @param[in] window The window whose text input focus state to set. + * @param[in] focused `GLFW_TRUE` to enter text input focus, or `GLFW_FALSE` + * to leave it. + * + * @errors Possible errors include @ref GLFW_NOT_INITIALIZED and @ref + * GLFW_PLATFORM_ERROR. + * + * @thread_safety This function must only be called from the main thread. + * + * @sa @ref ime_support + * @sa @ref glfwSetInputMode + * + * @since Added in GLFW 3.X. + * + * @ingroup input + */ +GLFWAPI void glfwSetTextInputFocus(GLFWwindow* window, int focused); + /*! @brief Returns the preedit candidate. * * This function returns the text and the text-count of the preedit candidate. diff --git a/src/cocoa_init.m b/src/cocoa_init.m index c982027047..f8423fe6cd 100644 --- a/src/cocoa_init.m +++ b/src/cocoa_init.m @@ -536,6 +536,7 @@ GLFWbool _glfwConnectCocoa(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringCocoa, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleCocoa, .resetPreeditText = _glfwResetPreeditTextCocoa, + .setTextInputFocus = _glfwSetTextInputFocusCocoa, .setIMEStatus = _glfwSetIMEStatusCocoa, .getIMEStatus = _glfwGetIMEStatusCocoa, .initJoysticks = _glfwInitJoysticksCocoa, diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h index fff4178b15..0a1625b7a7 100644 --- a/src/cocoa_platform.h +++ b/src/cocoa_platform.h @@ -300,6 +300,7 @@ const char* _glfwGetClipboardStringCocoa(void); void _glfwUpdatePreeditCursorRectangleCocoa(_GLFWwindow* window); void _glfwResetPreeditTextCocoa(_GLFWwindow* window); +void _glfwSetTextInputFocusCocoa(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusCocoa(_GLFWwindow* window, int active); int _glfwGetIMEStatusCocoa(_GLFWwindow* window); diff --git a/src/cocoa_window.m b/src/cocoa_window.m index d894b4a4e9..c09b63e7ce 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -2037,6 +2037,11 @@ void _glfwResetPreeditTextCocoa(_GLFWwindow* window) } // autoreleasepool } +void _glfwSetTextInputFocusCocoa(_GLFWwindow* window, GLFWbool focused) +{ + // TODO: Add a safe NSTextInputContext mapping without changing TIS behavior. +} + void _glfwSetIMEStatusCocoa(_GLFWwindow* window, int active) { @autoreleasepool { diff --git a/src/input.c b/src/input.c index 4507f22e97..96fa01c76c 100644 --- a/src/input.c +++ b/src/input.c @@ -1040,6 +1040,19 @@ GLFWAPI void glfwResetPreeditText(GLFWwindow* handle) _glfw.platform.resetPreeditText(window); } +GLFWAPI void glfwSetTextInputFocus(GLFWwindow* handle, int focused) +{ + _GLFW_REQUIRE_INIT(); + + _GLFWwindow* window = (_GLFWwindow*) handle; + assert(window != NULL); + + focused = focused ? GLFW_TRUE : GLFW_FALSE; + window->textInputFocusInitialized = GLFW_TRUE; + window->textInputFocus = focused; + _glfw.platform.setTextInputFocus(window, focused); +} + GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* handle, int index, int* textCount) { _GLFWwindow* window = (_GLFWwindow*) handle; diff --git a/src/internal.h b/src/internal.h index ca5c280b99..57b43315d7 100644 --- a/src/internal.h +++ b/src/internal.h @@ -592,6 +592,12 @@ struct _GLFWwindow GLFWbool stickyMouseButtons; GLFWbool lockKeyMods; GLFWbool disableMouseButtonLimit; + + // Preserve legacy text input behavior for backward compatibility until + // glfwSetTextInputFocus is used for this window. + GLFWbool textInputFocusInitialized; + GLFWbool textInputFocus; + int cursorMode; char mouseButtons[GLFW_MOUSE_BUTTON_LAST + 1]; char keys[GLFW_KEY_LAST + 1]; @@ -744,6 +750,7 @@ struct _GLFWplatform const char* (*getClipboardString)(void); void (*updatePreeditCursorRectangle)(_GLFWwindow*); void (*resetPreeditText)(_GLFWwindow*); + void (*setTextInputFocus)(_GLFWwindow*,GLFWbool); void (*setIMEStatus)(_GLFWwindow*,int); int (*getIMEStatus)(_GLFWwindow*); GLFWbool (*initJoysticks)(void); diff --git a/src/null_init.c b/src/null_init.c index 1cf0eccdf5..b0be07b433 100644 --- a/src/null_init.c +++ b/src/null_init.c @@ -57,6 +57,7 @@ GLFWbool _glfwConnectNull(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringNull, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleNull, .resetPreeditText = _glfwResetPreeditTextNull, + .setTextInputFocus = _glfwSetTextInputFocusNull, .setIMEStatus = _glfwSetIMEStatusNull, .getIMEStatus = _glfwGetIMEStatusNull, .initJoysticks = _glfwInitJoysticksNull, diff --git a/src/null_platform.h b/src/null_platform.h index bbd3ee42e4..0516c65331 100644 --- a/src/null_platform.h +++ b/src/null_platform.h @@ -272,6 +272,7 @@ int _glfwGetKeyScancodeNull(int key); void _glfwUpdatePreeditCursorRectangleNull(_GLFWwindow* window); void _glfwResetPreeditTextNull(_GLFWwindow* window); +void _glfwSetTextInputFocusNull(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusNull(_GLFWwindow* window, int active); int _glfwGetIMEStatusNull(_GLFWwindow* window); diff --git a/src/null_window.c b/src/null_window.c index efadbe18fa..61cf1e6047 100644 --- a/src/null_window.c +++ b/src/null_window.c @@ -559,6 +559,10 @@ void _glfwResetPreeditTextNull(_GLFWwindow* window) { } +void _glfwSetTextInputFocusNull(_GLFWwindow* window, GLFWbool focused) +{ +} + void _glfwSetIMEStatusNull(_GLFWwindow* window, int active) { } diff --git a/src/win32_init.c b/src/win32_init.c index b809cfe214..3598aa3db0 100644 --- a/src/win32_init.c +++ b/src/win32_init.c @@ -621,6 +621,7 @@ GLFWbool _glfwConnectWin32(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringWin32, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleWin32, .resetPreeditText = _glfwResetPreeditTextWin32, + .setTextInputFocus = _glfwSetTextInputFocusWin32, .setIMEStatus = _glfwSetIMEStatusWin32, .getIMEStatus = _glfwGetIMEStatusWin32, .initJoysticks = _glfwInitJoysticksWin32, diff --git a/src/win32_platform.h b/src/win32_platform.h index 4df8442f0f..c1e8abc32d 100644 --- a/src/win32_platform.h +++ b/src/win32_platform.h @@ -578,6 +578,7 @@ const char* _glfwGetClipboardStringWin32(void); void _glfwUpdatePreeditCursorRectangleWin32(_GLFWwindow* window); void _glfwResetPreeditTextWin32(_GLFWwindow* window); +void _glfwSetTextInputFocusWin32(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusWin32(_GLFWwindow* window, int active); int _glfwGetIMEStatusWin32(_GLFWwindow* window); diff --git a/src/win32_window.c b/src/win32_window.c index 55b9185f4b..284f882959 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -2872,6 +2872,11 @@ void _glfwResetPreeditTextWin32(_GLFWwindow* window) ImmReleaseContext(hWnd, hIMC); } +void _glfwSetTextInputFocusWin32(_GLFWwindow* window, GLFWbool focused) +{ + // TODO: Add safe IMM/TSF text input focus plumbing without changing IME status. +} + void _glfwSetIMEStatusWin32(_GLFWwindow* window, int active) { HWND hWnd = window->win32.handle; diff --git a/src/wl_init.c b/src/wl_init.c index 3c00b9df38..af506ef257 100644 --- a/src/wl_init.c +++ b/src/wl_init.c @@ -468,6 +468,7 @@ GLFWbool _glfwConnectWayland(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringWayland, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleWayland, .resetPreeditText = _glfwResetPreeditTextWayland, + .setTextInputFocus = _glfwSetTextInputFocusWayland, .setIMEStatus = _glfwSetIMEStatusWayland, .getIMEStatus = _glfwGetIMEStatusWayland, #if defined(GLFW_BUILD_LINUX_JOYSTICK) diff --git a/src/wl_platform.h b/src/wl_platform.h index dc4bc5deae..1d381bd3ea 100644 --- a/src/wl_platform.h +++ b/src/wl_platform.h @@ -718,6 +718,7 @@ const char* _glfwGetClipboardStringWayland(void); void _glfwUpdatePreeditCursorRectangleWayland(_GLFWwindow* window); void _glfwResetPreeditTextWayland(_GLFWwindow* window); +void _glfwSetTextInputFocusWayland(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusWayland(_GLFWwindow* window, int active); int _glfwGetIMEStatusWayland(_GLFWwindow* window); diff --git a/src/wl_window.c b/src/wl_window.c index d3591d224e..c6912f57b5 100644 --- a/src/wl_window.c +++ b/src/wl_window.c @@ -3932,6 +3932,11 @@ void _glfwResetPreeditTextWayland(_GLFWwindow* window) { } +void _glfwSetTextInputFocusWayland(_GLFWwindow* window, GLFWbool focused) +{ + // TODO: Wire this to text-input-v3 enable/disable or focus integration. +} + void _glfwSetIMEStatusWayland(_GLFWwindow* window, int active) { } diff --git a/src/x11_init.c b/src/x11_init.c index d2f0da0f93..737b911cbe 100644 --- a/src/x11_init.c +++ b/src/x11_init.c @@ -1190,6 +1190,7 @@ GLFWbool _glfwConnectX11(int platformID, _GLFWplatform* platform) .getClipboardString = _glfwGetClipboardStringX11, .updatePreeditCursorRectangle = _glfwUpdatePreeditCursorRectangleX11, .resetPreeditText = _glfwResetPreeditTextX11, + .setTextInputFocus = _glfwSetTextInputFocusX11, .setIMEStatus = _glfwSetIMEStatusX11, .getIMEStatus = _glfwGetIMEStatusX11, #if defined(GLFW_BUILD_LINUX_JOYSTICK) diff --git a/src/x11_platform.h b/src/x11_platform.h index 4d6693133d..293b91264d 100644 --- a/src/x11_platform.h +++ b/src/x11_platform.h @@ -981,6 +981,7 @@ const char* _glfwGetClipboardStringX11(void); void _glfwUpdatePreeditCursorRectangleX11(_GLFWwindow* window); void _glfwResetPreeditTextX11(_GLFWwindow* window); +void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused); void _glfwSetIMEStatusX11(_GLFWwindow* window, int active); int _glfwGetIMEStatusX11(_GLFWwindow* window); diff --git a/src/x11_window.c b/src/x11_window.c index dc6d3e5681..7d074fe338 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -3429,6 +3429,10 @@ void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) XUnsetICFocus(ic); } +void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused) +{ +} + int _glfwGetIMEStatusX11(_GLFWwindow* window) { if (!window->x11.ic) From 5863b8f546796da4c4f27dd9c7c7e2d06c0633f4 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:25:48 +0900 Subject: [PATCH 2/7] X11: Implement text input focus Map explicit text input focus to XIM input context focus with XSetICFocus and XUnsetICFocus. On focus out, reset preedit through the existing X11 helper before unsetting IC focus, preserving its conservative behavior. Native FocusIn only restores XIC focus when text input focus has not been explicitly disabled. --- src/x11_window.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/x11_window.c b/src/x11_window.c index 7d074fe338..4f4b3946c2 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -1961,8 +1961,11 @@ static void processEvent(XEvent *event) else if (window->cursorMode == GLFW_CURSOR_CAPTURED) captureCursor(window); - if (window->x11.ic) + if (window->x11.ic && + (!window->textInputFocusInitialized || window->textInputFocus)) + { XSetICFocus(window->x11.ic); + } _glfwInputWindowFocus(window, GLFW_TRUE); return; @@ -3431,6 +3434,18 @@ void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused) { + XIC ic = window->x11.ic; + + if (!ic) + return; + + if (focused) + XSetICFocus(ic); + else + { + _glfwResetPreeditTextX11(window); + XUnsetICFocus(ic); + } } int _glfwGetIMEStatusX11(_GLFWwindow* window) From b30a503e4efbd5e37fcfbb8ca578f473dafbc631 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:29:05 +0900 Subject: [PATCH 3/7] 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. --- src/win32_init.c | 2 + src/win32_platform.h | 4 ++ src/win32_window.c | 101 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 99 insertions(+), 8 deletions(-) diff --git a/src/win32_init.c b/src/win32_init.c index 3598aa3db0..248ace6b53 100644 --- a/src/win32_init.c +++ b/src/win32_init.c @@ -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) diff --git a/src/win32_platform.h b/src/win32_platform.h index c1e8abc32d..3c92278eac 100644 --- a/src/win32_platform.h +++ b/src/win32_platform.h @@ -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_; diff --git a/src/win32_window.c b/src/win32_window.c index 284f882959..c9ea008781 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -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; @@ -1145,6 +1153,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) @@ -1158,6 +1169,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. @@ -1167,6 +1181,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: @@ -1954,6 +1971,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; @@ -2856,6 +2876,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 } }; @@ -2868,29 +2891,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; } From 934a37bf1f9b68f39573bbf5160eef73c7af442a Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:29:59 +0900 Subject: [PATCH 4/7] Cocoa: Bypass text input system when unfocused When explicit text input focus is inactive, skip interpretKeyEvents and send plain event characters through GLFW text input instead. This prevents routing key events through NSTextInput composition while preserving normal key input. Applications that never call glfwSetTextInputFocus keep the previous interpretKeyEvents behavior. On focus out, discard marked text through the existing NSTextInputContext reset path. Do not use TIS input source switching for this API. This builds on the Cocoa input method work from clear-code/glfw#7. --- CONTRIBUTORS.md | 1 + src/cocoa_window.m | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 96369cf28e..21f545f6ac 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -88,6 +88,7 @@ video tutorials. - GeO4d - Marcus Geelnard - Gegy + - ghostflyby - ghuser404 - Charles Giessen - Ryan C. Gordon diff --git a/src/cocoa_window.m b/src/cocoa_window.m index c09b63e7ce..81b3549530 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -573,7 +573,14 @@ - (void)keyDown:(NSEvent *)event if (![self hasMarkedText]) _glfwInputKey(window, key, [event keyCode], GLFW_PRESS, mods); - [self interpretKeyEvents:@[event]]; + if (!window->textInputFocusInitialized || window->textInputFocus) + [self interpretKeyEvents:@[event]]; + else + { + NSString* characters = [event characters]; + if (characters) + [self insertText:characters replacementRange:[self selectedRange]]; + } } - (void)flagsChanged:(NSEvent *)event @@ -2039,7 +2046,8 @@ void _glfwResetPreeditTextCocoa(_GLFWwindow* window) void _glfwSetTextInputFocusCocoa(_GLFWwindow* window, GLFWbool focused) { - // TODO: Add a safe NSTextInputContext mapping without changing TIS behavior. + if (!focused) + _glfwResetPreeditTextCocoa(window); } void _glfwSetIMEStatusCocoa(_GLFWwindow* window, int active) From 1e8c9544591b684e60c4d4d8f7963124b7d637c4 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:31:10 +0900 Subject: [PATCH 5/7] Wayland: Gate text-input routing on focus Map explicit text input focus to text-input-v3 enable/disable and text-input-v1 activate/deactivate. When text input focus is inactive, avoid auto-enabling text input on protocol enter and ignore stale text-input callbacks. Applications that never call glfwSetTextInputFocus keep the previous enter-time activation behavior. On focus out, reset local preedit state and send the available protocol reset/disable requests. --- src/wl_window.c | 77 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/src/wl_window.c b/src/wl_window.c index c6912f57b5..129475b479 100644 --- a/src/wl_window.c +++ b/src/wl_window.c @@ -734,6 +734,11 @@ static void deactivateTextInputV1(_GLFWwindow* window) zwp_text_input_v1_deactivate(window->wl.textInputV1, _glfw.wl.seat); } +static GLFWbool textInputFocusDisabled(_GLFWwindow* window) +{ + return window->textInputFocusInitialized && !window->textInputFocus; +} + static void xdgToplevelHandleConfigure(void* userData, struct xdg_toplevel* toplevel, int32_t width, @@ -761,7 +766,8 @@ static void xdgToplevelHandleConfigure(void* userData, break; case XDG_TOPLEVEL_STATE_ACTIVATED: window->wl.pending.activated = GLFW_TRUE; - activateTextInputV1(window); + if (!textInputFocusDisabled(window)) + activateTextInputV1(window); break; } } @@ -1706,7 +1712,8 @@ static void pointerHandleButton(void* userData, // On weston, pressing the title bar will cause leave event and never emit // enter event even though back to content area by pressing mouse button // just after it. So activate it here explicitly. - activateTextInputV1(window); + if (!textInputFocusDisabled(window)) + activateTextInputV1(window); _glfw.wl.serial = serial; @@ -2399,8 +2406,13 @@ static void textInputV3Enter(void* data, struct zwp_text_input_v3* textInputV3, struct wl_surface* surface) { - zwp_text_input_v3_enable(textInputV3); - zwp_text_input_v3_commit(textInputV3); + _GLFWwindow* window = (_GLFWwindow*) data; + + if (!textInputFocusDisabled(window)) + { + zwp_text_input_v3_enable(textInputV3); + zwp_text_input_v3_commit(textInputV3); + } } static void textInputV3Reset(_GLFWwindow* window) @@ -2440,6 +2452,9 @@ static void textInputV3PreeditString(void* data, const char* cur = text; unsigned int cursorLength = 0; + if (textInputFocusDisabled(window)) + return; + preedit->textCount = 0; preedit->blockSizesCount = 0; preedit->focusedBlockIndex = 0; @@ -2513,6 +2528,9 @@ static void textInputV3CommitString(void* data, _GLFWwindow* window = (_GLFWwindow*) data; const char* cur = text; + if (textInputFocusDisabled(window)) + return; + if (!window->callbacks.character) return; @@ -2535,6 +2553,9 @@ static void textInputV3Done(void* data, uint32_t serial) { _GLFWwindow* window = (_GLFWwindow*) data; + if (textInputFocusDisabled(window)) + return; + _glfwUpdatePreeditCursorRectangleWayland(window); _glfwInputPreedit(window); } @@ -2560,7 +2581,9 @@ static void textInputV1Enter(void* data, struct wl_surface* surface) { _GLFWwindow* window = (_GLFWwindow*) data; - activateTextInputV1(window); + + if (!textInputFocusDisabled(window)) + activateTextInputV1(window); } static void textInputV1Reset(_GLFWwindow* window) @@ -2586,6 +2609,13 @@ static void textInputV1Leave(void* data, _GLFWwindow* window = (_GLFWwindow*) data; char* commitText = window->wl.textInputV1Context.commitTextOnReset; + if (textInputFocusDisabled(window)) + { + textInputV1Reset(window); + deactivateTextInputV1(window); + return; + } + textInputV3CommitString(data, NULL, commitText); textInputV1Reset(window); deactivateTextInputV1(window); @@ -2611,6 +2641,9 @@ static void textInputV1PreeditString(void* data, { _GLFWwindow* window = (_GLFWwindow*) data; + if (textInputFocusDisabled(window)) + return; + _glfw_free(window->wl.textInputV1Context.preeditText); _glfw_free(window->wl.textInputV1Context.commitTextOnReset); window->wl.textInputV1Context.preeditText = strdup(text); @@ -2637,6 +2670,9 @@ static void textInputV1PreeditCursor(void* data, const char* text = window->wl.textInputV1Context.preeditText; const char* cur = text; + if (textInputFocusDisabled(window)) + return; + preedit->caretIndex = 0; if (index <= 0 || preedit->textCount == 0) return; @@ -2659,6 +2695,9 @@ static void textInputV1CommitString(void* data, { _GLFWwindow* window = (_GLFWwindow*) data; + if (textInputFocusDisabled(window)) + return; + textInputV1Reset(window); textInputV3CommitString(data, NULL, text); } @@ -2686,6 +2725,10 @@ static void textInputV1Keysym(void* data, uint32_t state, uint32_t modifiers) { + _GLFWwindow* window = (_GLFWwindow*) data; + if (textInputFocusDisabled(window)) + return; + uint32_t scancode; // This code supports only weston-keyboard because we aren't aware @@ -3934,7 +3977,29 @@ void _glfwResetPreeditTextWayland(_GLFWwindow* window) void _glfwSetTextInputFocusWayland(_GLFWwindow* window, GLFWbool focused) { - // TODO: Wire this to text-input-v3 enable/disable or focus integration. + if (window->wl.textInputV3) + { + if (focused) + zwp_text_input_v3_enable(window->wl.textInputV3); + else + { + zwp_text_input_v3_disable(window->wl.textInputV3); + textInputV3Reset(window); + } + + zwp_text_input_v3_commit(window->wl.textInputV3); + } + else if (window->wl.textInputV1) + { + if (focused) + activateTextInputV1(window); + else + { + zwp_text_input_v1_reset(window->wl.textInputV1); + textInputV1Reset(window); + deactivateTextInputV1(window); + } + } } void _glfwSetIMEStatusWayland(_GLFWwindow* window, int active) From a1ca6f0bc098d1b5123eb3b0055757b49c261723 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 18:33:50 +0900 Subject: [PATCH 6/7] Add text input focus toggle to input_text test Expose glfwSetTextInputFocus in the input_text test so IME and text input focus behavior can be exercised independently. The UI starts in compatibility mode, then shows explicit Focus In or Focus Out after the toggle is used. --- tests/input_text.c | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/tests/input_text.c b/tests/input_text.c index cc006f2532..a315db28cf 100644 --- a/tests/input_text.c +++ b/tests/input_text.c @@ -112,6 +112,14 @@ static int fontNum = 0; static int currentFontIndex = 0; static int currentIMEStatus = GLFW_FALSE; +enum text_input_focus_status +{ + TEXT_INPUT_FOCUS_COMPATIBLE, + TEXT_INPUT_FOCUS_IN, + TEXT_INPUT_FOCUS_OUT +}; +static enum text_input_focus_status currentTextInputFocusStatus = + TEXT_INPUT_FOCUS_COMPATIBLE; #define MAX_PREEDIT_LEN 128 static char preeditBuf[MAX_PREEDIT_LEN] = ""; @@ -510,13 +518,27 @@ static int set_font_selecter(GLFWwindow* window, struct nk_context* nk, int heig static void set_ime_buttons(GLFWwindow* window, struct nk_context* nk, int height) { - nk_layout_row_dynamic(nk, height, 2); + nk_layout_row_dynamic(nk, height, 3); if (nk_button_label(nk, "Toggle IME status")) { glfwSetInputMode(window, GLFW_IME, !currentIMEStatus); } + if (nk_button_label(nk, "Toggle text input focus")) + { + if (currentTextInputFocusStatus == TEXT_INPUT_FOCUS_IN) + { + glfwSetTextInputFocus(window, GLFW_FALSE); + currentTextInputFocusStatus = TEXT_INPUT_FOCUS_OUT; + } + else + { + glfwSetTextInputFocus(window, GLFW_TRUE); + currentTextInputFocusStatus = TEXT_INPUT_FOCUS_IN; + } + } + if (nk_button_label(nk, "Reset preedit text")) { glfwResetPreeditText(window); @@ -615,8 +637,23 @@ static void set_preedit_cursor_edit(GLFWwindow* window, struct nk_context* nk, i static void set_ime_stauts_labels(GLFWwindow* window, struct nk_context* nk, int height) { - nk_layout_row_dynamic(nk, height, 1); + const char* textInputFocusStatus = "Text input focus: compatible"; + + switch (currentTextInputFocusStatus) + { + case TEXT_INPUT_FOCUS_IN: + textInputFocusStatus = "Text input focus: Focus In"; + break; + case TEXT_INPUT_FOCUS_OUT: + textInputFocusStatus = "Text input focus: Focus Out"; + break; + case TEXT_INPUT_FOCUS_COMPATIBLE: + break; + } + + nk_layout_row_dynamic(nk, height, 2); nk_value_bool(nk, "IME status", currentIMEStatus); + nk_label(nk, textInputFocusStatus, NK_TEXT_LEFT); } static void set_preedit_labels(GLFWwindow* window, struct nk_context* nk, int height) From 744c3de3658a9929bcb5378d3f373a37f046fb11 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Mon, 13 Jul 2026 22:09:27 +0900 Subject: [PATCH 7/7] Add text input focus changelog entry --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2241376b4b..4b3a023edf 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,8 @@ information on what to include when reporting a bug. area (#2130) - Added `glfwResetPreeditText` function to reset preedit of input method (#2130) + - Added `glfwSetTextInputFocus` function to set application text input + focus for input method handling (#2130) - Added `glfwSetPreeditCandidateCallback` function and `GLFWpreeditcandidatefun` type for preedit candidates (#2130) - Added `glfwGetPreeditCandidate` function to get a preeidt candidate text