From b25aa246eecaf3401698c810304c2bd02730138f Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:24:32 +0900 Subject: [PATCH 1/6] 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 textInputFocusExplicit, 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 | 52 +++++++++++++++++++++++++++++++++++++++++++- include/GLFW/glfw3.h | 51 ++++++++++++++++++++++++++++++++++++++++++- src/cocoa_init.m | 2 +- src/cocoa_platform.h | 2 +- src/cocoa_window.m | 6 ++++- src/input.c | 14 +++++++++++- src/internal.h | 14 +++++++++++- src/null_init.c | 2 +- src/null_platform.h | 2 +- src/null_window.c | 5 ++++- src/win32_init.c | 2 +- src/win32_platform.h | 2 +- src/win32_window.c | 6 ++++- src/wl_init.c | 2 +- src/wl_platform.h | 2 +- src/wl_window.c | 6 ++++- src/x11_init.c | 2 +- src/x11_platform.h | 2 +- src/x11_window.c | 5 ++++- 19 files changed, 160 insertions(+), 19 deletions(-) diff --git a/docs/input.md b/docs/input.md index a502ffa9f6..1880f3f8f4 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 @@ -1239,4 +1290,3 @@ void drop_callback(GLFWwindow* window, int count, const char** paths) The path array and its strings are only valid until the file drop callback returns, as they may have been generated specifically for that event. You need to make a deep copy of the array if you want to keep the paths. - diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index cb75ad99cd..ed36feb93b 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -5310,6 +5310,56 @@ GLFWAPI void glfwSetPreeditCursorRectangle(GLFWwindow* window, int x, int y, int */ GLFWAPI void glfwResetPreeditText(GLFWwindow* window); +/*! @brief Sets whether the application has text input focus. + * + * This function informs GLFW that the specified window has entered or left + * a text input context, such as a chat box, text field, search box, rename + * dialog or editor. Text input focus is separate from native window focus. + * A 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. + * + * Pass `GLFW_TRUE` when the application enters a text input context and + * `GLFW_FALSE` when it leaves that context. + * + * This function does not turn the IME on or off, switch input languages or + * force a specific platform input source. It expresses whether GLFW should + * route key and text input through the platform text input or IME path for + * this window. Individual platforms may map this abstraction differently. + * + * 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, that window enters explicit text input + * focus management and the application is responsible for notifying GLFW when + * text input begins and ends. + * Applications that opt into explicit text input focus management should set + * the initial state explicitly after window creation. + * + * This function is related to but distinct from + * `glfwSetInputMode(window, GLFW_IME, value)`. Text input focus describes + * application intent, while `GLFW_IME` controls platform-specific IME state. + * 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. @@ -6862,4 +6912,3 @@ GLFWAPI VkResult glfwCreateWindowSurface(VkInstance instance, GLFWwindow* window #endif #endif /* _glfw3_h_ */ - diff --git a/src/cocoa_init.m b/src/cocoa_init.m index c982027047..0674ebb401 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, @@ -723,4 +724,3 @@ void _glfwTerminateCocoa(void) } #endif // _GLFW_COCOA - diff --git a/src/cocoa_platform.h b/src/cocoa_platform.h index fff4178b15..44a620bd72 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); @@ -332,4 +333,3 @@ GLFWbool _glfwCreateContextNSGL(_GLFWwindow* window, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig); void _glfwDestroyContextNSGL(_GLFWwindow* window); - diff --git a/src/cocoa_window.m b/src/cocoa_window.m index d894b4a4e9..b9debfeda7 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 { @@ -2311,4 +2316,3 @@ GLFWAPI id glfwGetCocoaView(GLFWwindow* handle) } #endif // _GLFW_COCOA - diff --git a/src/input.c b/src/input.c index 4507f22e97..23391ad5f5 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->textInputFocusExplicit = GLFW_TRUE; + window->textInputFocus = focused; + _glfw.platform.setTextInputFocus(window, focused); +} + GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* handle, int index, int* textCount) { _GLFWwindow* window = (_GLFWwindow*) handle; @@ -1647,4 +1660,3 @@ GLFWAPI uint64_t glfwGetTimerFrequency(void) _GLFW_REQUIRE_INIT_OR_RETURN(0); return _glfwPlatformGetTimerFrequency(); } - diff --git a/src/internal.h b/src/internal.h index ca5c280b99..378fccb266 100644 --- a/src/internal.h +++ b/src/internal.h @@ -592,6 +592,18 @@ struct _GLFWwindow GLFWbool stickyMouseButtons; GLFWbool lockKeyMods; GLFWbool disableMouseButtonLimit; + + // Requested application text input focus state. This is meaningful only + // after textInputFocusExplicit has been set by glfwSetTextInputFocus. + // It describes whether an application-drawn text input context, such as a + // chat box or text field, currently has focus inside this native window. + GLFWbool textInputFocus; + // Whether the application has opted into explicit text input focus + // management for this window. This exists so that the zero-initialized + // textInputFocus value does not disable or bypass existing platform text + // input behavior for applications that never call glfwSetTextInputFocus. + GLFWbool textInputFocusExplicit; + int cursorMode; char mouseButtons[GLFW_MOUSE_BUTTON_LAST + 1]; char keys[GLFW_KEY_LAST + 1]; @@ -744,6 +756,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); @@ -1070,4 +1083,3 @@ int _glfw_max(int a, int b); void* _glfw_calloc(size_t count, size_t size); void* _glfw_realloc(void* pointer, size_t size); void _glfw_free(void* pointer); - diff --git a/src/null_init.c b/src/null_init.c index 1cf0eccdf5..05377d7170 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, @@ -266,4 +267,3 @@ void _glfwTerminateNull(void) _glfwTerminateEGL(); memset(&_glfw.null, 0, sizeof(_glfw.null)); } - diff --git a/src/null_platform.h b/src/null_platform.h index bbd3ee42e4..4b22634152 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); @@ -284,4 +285,3 @@ GLFWbool _glfwGetPhysicalDevicePresentationSupportNull(VkInstance instance, VkPh VkResult _glfwCreateWindowSurfaceNull(VkInstance instance, _GLFWwindow* window, const VkAllocationCallbacks* allocator, VkSurfaceKHR* surface); void _glfwPollMonitorsNull(void); - diff --git a/src/null_window.c b/src/null_window.c index efadbe18fa..108b564ed4 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) { } @@ -763,4 +767,3 @@ VkResult _glfwCreateWindowSurfaceNull(VkInstance instance, return err; } - diff --git a/src/win32_init.c b/src/win32_init.c index b809cfe214..7b60c4fc5d 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, @@ -741,4 +742,3 @@ void _glfwTerminateWin32(void) } #endif // _GLFW_WIN32 - diff --git a/src/win32_platform.h b/src/win32_platform.h index 4df8442f0f..060fd884ca 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); @@ -609,4 +610,3 @@ void _glfwTerminateWGL(void); GLFWbool _glfwCreateContextWGL(_GLFWwindow* window, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig); - diff --git a/src/win32_window.c b/src/win32_window.c index 55b9185f4b..72e907a119 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; @@ -3019,4 +3024,3 @@ GLFWAPI HWND glfwGetWin32Window(GLFWwindow* handle) } #endif // _GLFW_WIN32 - diff --git a/src/wl_init.c b/src/wl_init.c index 3c00b9df38..102c7462fc 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) @@ -1045,4 +1046,3 @@ void _glfwTerminateWayland(void) } #endif // _GLFW_WAYLAND - diff --git a/src/wl_platform.h b/src/wl_platform.h index dc4bc5deae..1b087721cc 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); @@ -745,4 +746,3 @@ void _glfwAddSeatListenerWayland(struct wl_seat* seat); void _glfwAddDataDeviceListenerWayland(struct wl_data_device* device); GLFWbool _glfwWaitForEGLFrameWayland(_GLFWwindow* window); - diff --git a/src/wl_window.c b/src/wl_window.c index d3591d224e..005c01e363 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) { } @@ -4059,4 +4064,3 @@ GLFWAPI struct wl_surface* glfwGetWaylandWindow(GLFWwindow* handle) } #endif // _GLFW_WAYLAND - diff --git a/src/x11_init.c b/src/x11_init.c index d2f0da0f93..720eed2216 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) @@ -1633,4 +1634,3 @@ void _glfwTerminateX11(void) } #endif // _GLFW_X11 - diff --git a/src/x11_platform.h b/src/x11_platform.h index 4d6693133d..2800105257 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); @@ -1032,4 +1033,3 @@ GLFWbool _glfwChooseVisualGLX(const _GLFWwndconfig* wndconfig, const _GLFWctxconfig* ctxconfig, const _GLFWfbconfig* fbconfig, Visual** visual, int* depth); - diff --git a/src/x11_window.c b/src/x11_window.c index dc6d3e5681..ab7aed6dc7 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) @@ -3715,4 +3719,3 @@ GLFWAPI const char* glfwGetX11SelectionString(void) } #endif // _GLFW_X11 - From 0f33471e212f61fc0dc592e8b5059146d6f93471 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:25:48 +0900 Subject: [PATCH 2/6] 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 | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/x11_window.c b/src/x11_window.c index ab7aed6dc7..0915be9744 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->textInputFocusExplicit || window->textInputFocus)) + { XSetICFocus(window->x11.ic); + } _glfwInputWindowFocus(window, GLFW_TRUE); return; @@ -3431,6 +3434,20 @@ void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused) { + XIC ic = window->x11.ic; + + if (!ic) + return; + + // This API reflects the text input focus abstraction discussed in the + // clear-code/glfw#5 and clear-code/glfw#7 IME support work. + if (focused) + XSetICFocus(ic); + else + { + _glfwResetPreeditTextX11(window); + XUnsetICFocus(ic); + } } int _glfwGetIMEStatusX11(_GLFWwindow* window) From 444d60daef7b51e7c774a301f234f84f98de4b06 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:29:05 +0900 Subject: [PATCH 3/6] 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 | 7 +++ src/win32_window.c | 101 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/win32_init.c b/src/win32_init.c index 7b60c4fc5d..dafd3d751a 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 060fd884ca..a2cea9ede0 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,10 @@ typedef struct _GLFWlibraryWGL typedef struct _GLFWwindowWin32 { HWND handle; + // Saved HIMC while explicit text input focus is out. A NULL HIMC is + // associated with the HWND during that period to keep IMM from routing key + // input through composition and candidate UI. + HIMC textInputContext; HICON bigIcon; HICON smallIcon; @@ -473,6 +479,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 72e907a119..28b2bcf95e 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->textInputFocusExplicit && !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 073eefc80dd08ea04b652961c47889eda5ee7c20 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:29:59 +0900 Subject: [PATCH 4/6] 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. --- src/cocoa_window.m | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cocoa_window.m b/src/cocoa_window.m index b9debfeda7..1caa41bfb4 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->textInputFocusExplicit || 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 282b185c14ca9eade1afc87d5160f68a0f1a3204 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:31:10 +0900 Subject: [PATCH 5/6] 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 005c01e363..e3f13d40a3 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->textInputFocusExplicit && !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 f663739fdde0f683c7e1fba1da06c446ff26f066 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 18:33:50 +0900 Subject: [PATCH 6/6] 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)