From b25aa246eecaf3401698c810304c2bd02730138f Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sun, 21 Jun 2026 16:24:32 +0900 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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 476afc8da2531848bea256a01e1470f2ac333c3c Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sat, 20 Jun 2026 10:35:33 +0900 Subject: [PATCH 07/12] X11: Add experimental IBus IME module prototype This adds a dynamically loaded X11 IME module ABI and an experimental glfw-ibus.so backend for feasibility testing only. The module owns D-Bus and worker-thread communication, while GLFW drains queued IME events on the main thread without modifying the event loop architecture. The prototype includes preedit and commit handling, cursor location updates for IBus SetCursorLocation, the first-candidate-window positioning workaround, GLFW_IME_DEBUG instrumentation, and documentation of the architecture and known timeout risks. --- docs/ime-ibus-prototype.md | 305 ++++++++++ src/CMakeLists.txt | 26 +- src/x11_ibus_module.c | 1101 ++++++++++++++++++++++++++++++++++++ src/x11_ime_module.c | 497 ++++++++++++++++ src/x11_ime_module.h | 97 ++++ src/x11_init.c | 17 +- src/x11_platform.h | 18 + src/x11_window.c | 80 ++- 8 files changed, 2133 insertions(+), 8 deletions(-) create mode 100644 docs/ime-ibus-prototype.md create mode 100644 src/x11_ibus_module.c create mode 100644 src/x11_ime_module.c create mode 100644 src/x11_ime_module.h diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md new file mode 100644 index 0000000000..d902043a14 --- /dev/null +++ b/docs/ime-ibus-prototype.md @@ -0,0 +1,305 @@ +# X11 IBus IME Prototype + +## Purpose + +This document describes an experimental X11 IME backend for GLFW based on IBus. +The goal of the prototype is to determine whether IBus, and Fcitx5 through its +IBus compatibility layer, can be used from GLFW without changing the GLFW event +loop architecture. + +This is a research prototype. It is not intended to be production quality or +ready for upstream submission. + +The prototype is meant to answer these questions: + +- Can an IME backend be loaded dynamically instead of linked into GLFW? +- Can all D-Bus communication happen outside the GLFW event loop? +- Can X11 key handling synchronously ask the IME whether a key was handled? +- Do IBus replies or text signals arrive late enough to cause duplicated text? +- Is candidate window positioning practical with the existing cursor rectangle + API? + +## Architecture + +The prototype adds a private X11-only IME module ABI. It is not part of the +public GLFW API. + +At runtime, GLFW checks `GLFW_IM_MODULE`. If it is set, GLFW loads the named +shared object with `dlopen()` and looks up the `glfwGetX11IMEBackend` symbol with +`dlsym()`. + +For example: + +```sh +GLFW_IM_MODULE=/path/to/glfw-ibus.so ./application +``` + +### GLFW Core + +GLFW core remains unaware of IBus, Fcitx5 and D-Bus. The X11 backend only knows +about the private plugin ABI in `src/x11_ime_module.h`. + +When a module is active, the X11 backend: + +- skips XIM setup for that run +- forwards X11 key events to the module +- forwards focus changes to the module +- translates preedit cursor rectangles from client-area coordinates to X11 root + coordinates +- drains queued module events on the main thread + +The public IME API semantics are unchanged. Applications still use +`glfwSetPreeditCursorRectangle` with GLFW window/client-area coordinates. + +### Plugin ABI + +The plugin ABI consists of: + +- a host callback table provided by GLFW +- a backend function table provided by the module +- opaque window tokens passed back to GLFW by the module + +The module must not dereference GLFW window pointers. They are only handles for +callbacks into GLFW. + +The module can request that `glfwWaitEvents` wake up by calling the host +`post_empty_event` callback. This uses GLFW's existing X11 empty-event pipe; it +does not add D-Bus file descriptors to the GLFW event loop. + +### Dynamically Loaded Module + +The prototype module is built as `glfw-ibus.so` when the `dbus-1` development +package is available. + +The module owns: + +- IBus address discovery +- libdbus connection setup +- IBus input context creation +- D-Bus method calls +- D-Bus signal handling +- worker thread lifetime +- request and event queues +- timing instrumentation + +### Worker Thread + +The module creates a worker thread. The worker thread owns all D-Bus traffic. + +GLFW's X11 thread sends commands to the worker through an explicit queue. The +worker sends IME events back through a second queue. The worker never calls GLFW +IME callbacks directly. + +Queued events are drained from GLFW's normal X11 event functions on the main +thread. This preserves the existing `glfwPollEvents`, `glfwWaitEvents` and +`glfwWaitEventsTimeout` architecture. + +### D-Bus Communication + +The worker uses libdbus directly. The prototype intentionally does not integrate +libdbus watches or timeouts with GLFW. + +`ProcessKeyEvent` is currently sent with a blocking D-Bus call on the worker +thread. The GLFW/X11 thread waits for the worker to report the result, with a +prototype timeout controlled by `GLFW_IBUS_TIMEOUT_MS`. + +The default timeout is 100 ms. + +### IBus Integration + +The module uses these IBus input context methods and signals: + +- `CreateInputContext` +- `SetCapabilities` +- `ProcessKeyEvent` +- `CommitText` +- `UpdatePreeditText` +- `HidePreeditText` +- `FocusIn` +- `FocusOut` +- `SetCursorLocation` +- `Reset` + +The module currently uses `SetCursorLocation`, not +`SetCursorLocationRelative`. + +For X11 candidate positioning, GLFW translates the application-provided cursor +rectangle from client-area coordinates to root-window coordinates with +`XTranslateCoordinates()` before sending it to the module. + +## Relationship to PR #2130 + +This prototype builds on the IME architecture introduced by PR #2130. + +It reuses: + +- the public `GLFW_IME` input mode +- preedit callbacks +- IME status callbacks +- preedit cursor rectangle APIs +- shared preedit state in `_GLFWpreedit` +- X11 platform IME hooks for focus, key handling, cursor rectangle updates and + reset + +It does not redesign the application-facing IME API. + +The prototype adds an alternative X11 IME backend path behind a dynamically +loaded module. When no module is loaded, the existing XIM behavior remains the +fallback. + +## Why A Plugin Architecture Was Chosen + +IBus support brings Linux/X11-specific complexity into an otherwise portable +library. A plugin boundary keeps that complexity separate from GLFW core. + +The plugin design was chosen to: + +- avoid a hard D-Bus dependency in GLFW core +- avoid libdbus watch and timeout integration in the GLFW event loop +- avoid changes to `glfwPollEvents`, `glfwWaitEvents` and + `glfwWaitEventsTimeout` +- isolate IBus/Fcitx5 behavior and failure modes +- make the experiment opt-in with `GLFW_IM_MODULE` +- allow the prototype to be removed or replaced without affecting the public API + +This matches the goal of evaluating feasibility without committing GLFW to a +production IBus backend design. + +## Current Status + +### Preedit Support + +Preedit updates from IBus are received through `UpdatePreeditText`, queued by the +worker and drained on the GLFW main thread. + +The prototype maps preedit text to a single focused block. This is sufficient +for testing text flow and candidate positioning, but it is not a complete IBus +attribute implementation. + +### Commit Support + +Committed text from `CommitText` is queued by the worker and emitted through +GLFW's normal character input path on the main thread. + +### Candidate Window Positioning + +Candidate window positioning is supported through `SetCursorLocation`. + +Applications still provide cursor rectangles in GLFW window/client-area +coordinates. The X11 plugin bridge translates those coordinates to root-window +coordinates before sending them to IBus. + +The bridge tracks whether a valid cursor rectangle has been translated. It does +not send `SetCursorLocation` until a valid rectangle exists. It resends the last +valid rectangle on focus and before key processing. + +### IME Enable And Disable Behavior + +The prototype has minimal IME status support. + +IBus `Enabled` and `Disabled` signals update module state and trigger GLFW IME +status callbacks. `glfwSetInputMode(window, GLFW_IME, value)` maps to +`FocusIn` or `FocusOut` in the prototype. + +This does not have the same semantics as Win32 `ImmGetOpenStatus` and +`ImmSetOpenStatus`. It is sufficient for experimentation, but not a final API +mapping. + +## Instrumentation + +The prototype intentionally keeps timing and late-event instrumentation. It is +emitted only when `GLFW_IME_DEBUG` is set to a non-zero value. + +Each `ProcessKeyEvent` request logs: + +- request id +- X11 key serial +- timestamp +- keyval +- keycode +- IBus state + +Each reply logs: + +- request id +- latency +- handled status +- timeout status +- failure status + +Each timeout logs: + +- request id +- elapsed time +- X11 key serial + +Each queued and drained IME event logs: + +- event type +- attributed request id +- whether the attributed request had timed out +- timestamp +- text, when present + +IBus signals do not include the originating `ProcessKeyEvent` request. The +module attributes signals to the active request when possible, otherwise to the +most recent request. This attribution is for observation only. + +## Known Risks + +### Timeout Semantics + +The GLFW/X11 thread may stop waiting before the worker receives a +`ProcessKeyEvent` reply. The key is then treated as not handled and GLFW falls +back to the normal X11 key path. + +IBus may still later process the key. + +### Possible Duplicated Text After Timeout + +If GLFW falls back to normal text input after a timeout and IBus later emits +`CommitText` for the same key, the application may receive duplicated text. + +The prototype is instrumented to observe this. It does not fully solve it. + +### Late ProcessKeyEvent Replies + +Late replies are logged with their original request id and timeout state. + +The prototype keeps a small list of recent requests so late replies and related +signals can be identified. + +### Late CommitText Events + +Late `CommitText` events are queued and logged with the best available request +attribution. Because IBus does not identify the originating request, this +attribution is not guaranteed to be exact. + +### Candidate And Surrounding Text Completeness + +The prototype does not implement lookup-table/candidate-list parsing, +surrounding text, or full IBus preedit attributes. These would be needed for a +more complete backend. + +### Restart And Recovery + +The prototype does not attempt production-grade daemon restart handling, +reconnection or failure recovery. + +## Current Recommendation + +The prototype demonstrates that the architecture is technically feasible: + +- an IME backend can be dynamically loaded +- D-Bus communication can be isolated from the GLFW event loop +- worker-thread communication can be kept behind explicit queues +- IBus/Fcitx5 preedit and commit paths can be integrated with the existing IME + architecture +- candidate window positioning can be handled without changing the public IME API + +However, this is still a research prototype. It is not currently recommended +for upstream submission. + +The remaining decision point is semantic reliability: late replies and late text +signals after key-processing timeouts need more real-world measurement before an +upstream-quality design can be justified. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bcd3871d6d..5b7a0d63f9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -46,7 +46,8 @@ endif() if (GLFW_BUILD_X11) target_compile_definitions(glfw PRIVATE _GLFW_X11) - target_sources(glfw PRIVATE x11_platform.h x11_init.c + target_sources(glfw PRIVATE x11_platform.h x11_ime_module.h + x11_ime_module.c x11_init.c x11_monitor.c x11_window.c xkb_unicode.c glx_context.c) endif() @@ -213,6 +214,28 @@ if (GLFW_BUILD_X11) message(FATAL_ERROR "X Shape headers not found; install libxext development package") endif() target_include_directories(glfw PRIVATE "${X11_Xshape_INCLUDE_PATH}") + + if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + include(FindPkgConfig) + pkg_check_modules(DBUS1 dbus-1) + if (DBUS1_FOUND) + add_library(glfw-ibus MODULE x11_ibus_module.c x11_ime_module.h) + set_target_properties(glfw-ibus PROPERTIES + PREFIX "" + C_STANDARD 99 + C_EXTENSIONS OFF + FOLDER "GLFW3") + target_include_directories(glfw-ibus PRIVATE + "${GLFW_SOURCE_DIR}/src" + "${GLFW_SOURCE_DIR}/include" + ${DBUS1_INCLUDE_DIRS}) + target_link_libraries(glfw-ibus PRIVATE Threads::Threads ${DBUS1_LIBRARIES}) + target_compile_options(glfw-ibus PRIVATE ${DBUS1_CFLAGS_OTHER}) + target_link_directories(glfw-ibus PRIVATE ${DBUS1_LIBRARY_DIRS}) + else() + message(STATUS "D-Bus development package not found; skipping glfw-ibus prototype module") + endif() + endif() endif() if (UNIX AND NOT APPLE) @@ -345,4 +368,3 @@ if (GLFW_INSTALL) ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") endif() - diff --git a/src/x11_ibus_module.c b/src/x11_ibus_module.c new file mode 100644 index 0000000000..fb0badfbff --- /dev/null +++ b/src/x11_ibus_module.c @@ -0,0 +1,1101 @@ +#define _POSIX_C_SOURCE 200809L + +//======================================================================== +// GLFW X11 IBus IME module prototype +//======================================================================== + +#include "x11_ime_module.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* IBUS_SERVICE = "org.freedesktop.IBus"; +static const char* IBUS_PATH = "/org/freedesktop/IBus"; +static const char* IBUS_INTERFACE = "org.freedesktop.IBus"; +static const char* IBUS_INPUT_INTERFACE = "org.freedesktop.IBus.InputContext"; + +enum +{ + IBUS_CAP_PREEDIT_TEXT = 1 << 0, + IBUS_CAP_FOCUS = 1 << 3 +}; + +enum +{ + IBUS_SHIFT_MASK = 1 << 0, + IBUS_LOCK_MASK = 1 << 1, + IBUS_CONTROL_MASK = 1 << 2, + IBUS_MOD1_MASK = 1 << 3, + IBUS_MOD2_MASK = 1 << 4, + IBUS_MOD4_MASK = 1 << 6, + IBUS_RELEASE_MASK = 1 << 30 +}; + +typedef enum CommandType +{ + COMMAND_KEY, + COMMAND_FOCUS_IN, + COMMAND_FOCUS_OUT, + COMMAND_CURSOR_RECT, + COMMAND_RESET, + COMMAND_SET_STATUS, + COMMAND_STOP +} CommandType; + +typedef enum EventType +{ + EVENT_COMMIT, + EVENT_PREEDIT, + EVENT_CLEAR_PREEDIT, + EVENT_STATUS +} EventType; + +typedef struct Request +{ + unsigned long id; + GLFWx11IMEKeyEvent event; + int completed; + int handled; + int timed_out; + int failed; + double queued_at; + double completed_at; + int refs; + struct Request* next_recent; +} Request; + +typedef struct Command +{ + CommandType type; + void* window; + unsigned long x11_window; + int x, y, w, h; + int status; + Request* request; + struct Command* next; +} Command; + +typedef struct QueuedEvent +{ + EventType type; + void* window; + char* text; + int caret; + unsigned long request_id; + int request_timed_out; + double timestamp; + struct QueuedEvent* next; +} QueuedEvent; + +struct GLFWx11IMEBackend +{ + GLFWx11IMEHostAPI host; + pthread_t thread; + pthread_mutex_t mutex; + pthread_cond_t cond; + int running; + int ready; + int status; + double timeout_ms; + void* focused_window; + void* active_window; + unsigned long next_request_id; + unsigned long active_request_id; + unsigned long last_request_id; + // Commands are written by the GLFW/X11 thread and consumed by the worker. + Command* command_head; + Command* command_tail; + // Events are written by the worker and drained by GLFW on the main thread. + // The worker never calls GLFW callbacks directly. + QueuedEvent* event_head; + QueuedEvent* event_tail; + // Recent requests are retained only to observe late replies and signals + // after a ProcessKeyEvent timeout. + Request* recent; + DBusConnection* connection; + char* input_context_path; +}; + +static char* xstrdup(const char* string) +{ + size_t length; + char* copy; + + if (!string) + return NULL; + + length = strlen(string) + 1; + copy = malloc(length); + if (copy) + memcpy(copy, string, length); + return copy; +} + +static double now_seconds(GLFWx11IMEBackend* backend) +{ + if (backend->host.get_time) + return backend->host.get_time(); + + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec / 1000000000.0; +} + +static void log_line(GLFWx11IMEBackend* backend, const char* fmt, ...) +{ + char buffer[1024]; + const char* debug = getenv("GLFW_IME_DEBUG"); + va_list vl; + va_start(vl, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, vl); + va_end(vl); + + if (backend->host.log) + backend->host.log(buffer); + else if (debug && *debug && strcmp(debug, "0") != 0) + fprintf(stderr, "glfw-ibus: %s\n", buffer); +} + +static uint32_t ibus_state_from_glfw(unsigned int mods, int action) +{ + uint32_t state = action == GLFW_RELEASE ? IBUS_RELEASE_MASK : 0; + + if (mods & GLFW_MOD_SHIFT) + state |= IBUS_SHIFT_MASK; + if (mods & GLFW_MOD_CAPS_LOCK) + state |= IBUS_LOCK_MASK; + if (mods & GLFW_MOD_CONTROL) + state |= IBUS_CONTROL_MASK; + if (mods & GLFW_MOD_ALT) + state |= IBUS_MOD1_MASK; + if (mods & GLFW_MOD_NUM_LOCK) + state |= IBUS_MOD2_MASK; + if (mods & GLFW_MOD_SUPER) + state |= IBUS_MOD4_MASK; + + return state; +} + +static void release_request(GLFWx11IMEBackend* backend, Request* request) +{ + int refs; + + if (!request) + return; + + refs = --request->refs; + if (refs == 0) + free(request); + (void) backend; +} + +static Request* find_recent(GLFWx11IMEBackend* backend, unsigned long id) +{ + for (Request* request = backend->recent; request; request = request->next_recent) + { + if (request->id == id) + return request; + } + + return NULL; +} + +static void remember_recent(GLFWx11IMEBackend* backend, Request* request) +{ + int count = 0; + Request* prev = NULL; + Request* item; + + request->refs++; + request->next_recent = backend->recent; + backend->recent = request; + + item = backend->recent; + while (item) + { + count++; + if (count > 64) + { + Request* old = item; + if (prev) + prev->next_recent = NULL; + while (old) + { + Request* next = old->next_recent; + old->next_recent = NULL; + release_request(backend, old); + old = next; + } + break; + } + prev = item; + item = item->next_recent; + } +} + +static void enqueue_event(GLFWx11IMEBackend* backend, + EventType type, + void* window, + const char* text, + int caret) +{ + QueuedEvent* event = calloc(1, sizeof(QueuedEvent)); + unsigned long request_id; + Request* request; + + if (!event) + return; + + pthread_mutex_lock(&backend->mutex); + + // IBus signals do not identify the ProcessKeyEvent that caused them. + // For instrumentation we attribute them to the active request, falling + // back to the most recent request, and record whether that request timed out. + request_id = backend->active_request_id ? backend->active_request_id : + backend->last_request_id; + request = find_recent(backend, request_id); + + event->type = type; + event->window = window ? window : + request ? request->event.window : + backend->active_window ? backend->active_window : + backend->focused_window; + event->text = text ? xstrdup(text) : NULL; + event->caret = caret; + event->request_id = request_id; + event->request_timed_out = request ? request->timed_out : 0; + event->timestamp = now_seconds(backend); + + if (backend->event_tail) + backend->event_tail->next = event; + else + backend->event_head = event; + backend->event_tail = event; + + pthread_mutex_unlock(&backend->mutex); + + log_line(backend, + "event queued type=%i request=%lu timed_out=%i timestamp=%.6f text='%s'", + type, event->request_id, event->request_timed_out, + event->timestamp, text ? text : ""); + + if (backend->host.post_empty_event) + backend->host.post_empty_event(); +} + +static Command* pop_command(GLFWx11IMEBackend* backend) +{ + Command* command = backend->command_head; + if (command) + { + backend->command_head = command->next; + if (!backend->command_head) + backend->command_tail = NULL; + } + return command; +} + +static void push_command(GLFWx11IMEBackend* backend, Command* command) +{ + pthread_mutex_lock(&backend->mutex); + if (backend->command_tail) + backend->command_tail->next = command; + else + backend->command_head = command; + backend->command_tail = command; + pthread_cond_signal(&backend->cond); + pthread_mutex_unlock(&backend->mutex); +} + +static int call_no_reply(GLFWx11IMEBackend* backend, const char* method, int first_type, ...) +{ + DBusMessage* message; + va_list vl; + dbus_uint32_t serial = 0; + + if (!backend->connection || !backend->input_context_path) + return GLFW_FALSE; + + message = dbus_message_new_method_call(IBUS_SERVICE, + backend->input_context_path, + IBUS_INPUT_INTERFACE, + method); + if (!message) + return GLFW_FALSE; + + va_start(vl, first_type); + if (first_type != DBUS_TYPE_INVALID && + !dbus_message_append_args_valist(message, first_type, vl)) + { + va_end(vl); + dbus_message_unref(message); + return GLFW_FALSE; + } + va_end(vl); + + if (!dbus_connection_send(backend->connection, message, &serial)) + { + dbus_message_unref(message); + return GLFW_FALSE; + } + + dbus_connection_flush(backend->connection); + dbus_message_unref(message); + return GLFW_TRUE; +} + +static const char* parse_ibus_text(DBusMessage* message) +{ + DBusMessageIter iter, variant, structure; + const char* id = NULL; + const char* text = NULL; + + dbus_message_iter_init(message, &iter); + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) + return NULL; + + dbus_message_iter_recurse(&iter, &variant); + if (dbus_message_iter_get_arg_type(&variant) != DBUS_TYPE_STRUCT) + return NULL; + + dbus_message_iter_recurse(&variant, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return NULL; + + dbus_message_iter_get_basic(&structure, &id); + if (!id || strcmp(id, "IBusText") != 0) + return NULL; + + dbus_message_iter_next(&structure); + dbus_message_iter_next(&structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return NULL; + + dbus_message_iter_get_basic(&structure, &text); + return text; +} + +static DBusHandlerResult dbus_filter(DBusConnection* connection, + DBusMessage* message, + void* data) +{ + GLFWx11IMEBackend* backend = data; + const char* text; + (void) connection; + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "CommitText")) + { + text = parse_ibus_text(message); + enqueue_event(backend, EVENT_COMMIT, NULL, text ? text : "", -1); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "UpdatePreeditText")) + { + text = parse_ibus_text(message); + enqueue_event(backend, EVENT_PREEDIT, NULL, text ? text : "", -1); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "HidePreeditText")) + { + enqueue_event(backend, EVENT_CLEAR_PREEDIT, NULL, NULL, -1); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "Enabled")) + { + backend->status = GLFW_TRUE; + enqueue_event(backend, EVENT_STATUS, NULL, NULL, -1); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "Disabled")) + { + backend->status = GLFW_FALSE; + enqueue_event(backend, EVENT_STATUS, NULL, NULL, -1); + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static int read_ibus_address(char* buffer, size_t size) +{ + const char* address = getenv("IBUS_ADDRESS"); + char path[4096]; + char display[128]; + const char* config; + const char* home; + char* machine_id; + DBusError error; + FILE* file; + + if (address && *address) + { + snprintf(buffer, size, "%s", address); + return GLFW_TRUE; + } + + snprintf(display, sizeof(display), "%s", getenv("DISPLAY") ? getenv("DISPLAY") : ":0.0"); + char* colon = strrchr(display, ':'); + if (!colon) + return GLFW_FALSE; + + char* screen = strrchr(display, '.'); + if (screen) + *screen = '\0'; + + *colon = '\0'; + const char* host = *display ? display : "unix"; + const char* number = colon + 1; + + config = getenv("XDG_CONFIG_HOME"); + home = getenv("HOME"); + + dbus_error_init(&error); + machine_id = dbus_try_get_local_machine_id(&error); + if (!machine_id) + { + dbus_error_free(&error); + return GLFW_FALSE; + } + + if (config && *config) + snprintf(path, sizeof(path), "%s/ibus/bus/%s-%s-%s", config, machine_id, host, number); + else if (home && *home) + snprintf(path, sizeof(path), "%s/.config/ibus/bus/%s-%s-%s", home, machine_id, host, number); + else + { + dbus_free(machine_id); + return GLFW_FALSE; + } + + dbus_free(machine_id); + + file = fopen(path, "r"); + if (!file) + return GLFW_FALSE; + + while (fgets(buffer, size, file)) + { + if (strncmp(buffer, "IBUS_ADDRESS=", 13) == 0) + { + char* value = buffer + 13; + char* newline = strchr(value, '\n'); + if (newline) + *newline = '\0'; + memmove(buffer, value, strlen(value) + 1); + fclose(file); + return GLFW_TRUE; + } + } + + fclose(file); + return GLFW_FALSE; +} + +static int connect_ibus(GLFWx11IMEBackend* backend) +{ + char address[1024]; + DBusError error; + DBusMessage* message; + DBusMessage* reply; + const char* client = "GLFW research prototype"; + const char* path = NULL; + dbus_uint32_t caps = IBUS_CAP_FOCUS | IBUS_CAP_PREEDIT_TEXT; + + if (!read_ibus_address(address, sizeof(address))) + { + log_line(backend, "failed to discover IBus address"); + return GLFW_FALSE; + } + + dbus_error_init(&error); + backend->connection = dbus_connection_open_private(address, &error); + if (!backend->connection) + { + log_line(backend, "failed to open IBus connection: %s", error.message ? error.message : ""); + dbus_error_free(&error); + return GLFW_FALSE; + } + + dbus_connection_set_exit_on_disconnect(backend->connection, FALSE); + if (!dbus_bus_register(backend->connection, &error)) + { + log_line(backend, "failed to register IBus bus connection: %s", error.message ? error.message : ""); + dbus_error_free(&error); + return GLFW_FALSE; + } + + message = dbus_message_new_method_call(IBUS_SERVICE, IBUS_PATH, + IBUS_INTERFACE, "CreateInputContext"); + if (!message) + return GLFW_FALSE; + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &client, + DBUS_TYPE_INVALID); + reply = dbus_connection_send_with_reply_and_block(backend->connection, + message, 3000, &error); + dbus_message_unref(message); + if (!reply) + { + log_line(backend, "CreateInputContext failed: %s", error.message ? error.message : ""); + dbus_error_free(&error); + return GLFW_FALSE; + } + + if (!dbus_message_get_args(reply, &error, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID)) + { + log_line(backend, "CreateInputContext reply parse failed: %s", error.message ? error.message : ""); + dbus_error_free(&error); + dbus_message_unref(reply); + return GLFW_FALSE; + } + + backend->input_context_path = xstrdup(path); + dbus_message_unref(reply); + + dbus_bus_add_match(backend->connection, + "type='signal',interface='org.freedesktop.IBus.InputContext'", + NULL); + dbus_connection_add_filter(backend->connection, dbus_filter, backend, NULL); + call_no_reply(backend, "SetCapabilities", + DBUS_TYPE_UINT32, &caps, + DBUS_TYPE_INVALID); + backend->ready = GLFW_TRUE; + backend->status = GLFW_TRUE; + + log_line(backend, "connected to IBus at %s path=%s", address, + backend->input_context_path ? backend->input_context_path : ""); + return GLFW_TRUE; +} + +static void process_key_command(GLFWx11IMEBackend* backend, Command* command) +{ + Request* request = command->request; + DBusError error; + DBusMessage* message; + DBusMessage* reply; + dbus_bool_t handled = FALSE; + dbus_uint32_t keyval = request->event.keysym; + dbus_uint32_t keycode = request->event.keycode; + dbus_uint32_t state = ibus_state_from_glfw(request->event.mods, + request->event.action); + + pthread_mutex_lock(&backend->mutex); + backend->active_request_id = request->id; + backend->active_window = request->event.window; + backend->last_request_id = request->id; + pthread_mutex_unlock(&backend->mutex); + + log_line(backend, + "request start id=%lu key_serial=%lu timestamp=%.6f keyval=0x%x keycode=%u state=0x%x", + request->id, request->event.time, request->queued_at, + keyval, keycode, state); + + if (request->event.cursor_rect_valid) + { + log_line(backend, + "request cursor id=%lu valid=1 original=(%i,%i %ix%i) root=(%i,%i %ix%i) sent_before_process=1", + request->id, + request->event.cursor_x, + request->event.cursor_y, + request->event.cursor_width, + request->event.cursor_height, + request->event.cursor_root_x, + request->event.cursor_root_y, + request->event.cursor_width, + request->event.cursor_height); + call_no_reply(backend, "SetCursorLocation", + DBUS_TYPE_INT32, &request->event.cursor_root_x, + DBUS_TYPE_INT32, &request->event.cursor_root_y, + DBUS_TYPE_INT32, &request->event.cursor_width, + DBUS_TYPE_INT32, &request->event.cursor_height, + DBUS_TYPE_INVALID); + } + else + { + log_line(backend, + "request cursor id=%lu valid=0 sent_before_process=0", + request->id); + } + + dbus_error_init(&error); + message = dbus_message_new_method_call(IBUS_SERVICE, + backend->input_context_path, + IBUS_INPUT_INTERFACE, + "ProcessKeyEvent"); + if (message && + dbus_message_append_args(message, + DBUS_TYPE_UINT32, &keyval, + DBUS_TYPE_UINT32, &keycode, + DBUS_TYPE_UINT32, &state, + DBUS_TYPE_INVALID)) + { + reply = dbus_connection_send_with_reply_and_block(backend->connection, + message, 3000, &error); + if (reply) + { + dbus_message_get_args(reply, &error, + DBUS_TYPE_BOOLEAN, &handled, + DBUS_TYPE_INVALID); + dbus_message_unref(reply); + } + else + request->failed = GLFW_TRUE; + } + else + request->failed = GLFW_TRUE; + + if (message) + dbus_message_unref(message); + + if (dbus_error_is_set(&error)) + { + log_line(backend, "request error id=%lu error=%s", request->id, + error.message ? error.message : ""); + dbus_error_free(&error); + request->failed = GLFW_TRUE; + } + + pthread_mutex_lock(&backend->mutex); + backend->active_request_id = 0; + backend->active_window = NULL; + request->handled = handled ? GLFW_TRUE : GLFW_FALSE; + request->completed = GLFW_TRUE; + request->completed_at = now_seconds(backend); + remember_recent(backend, request); + pthread_cond_broadcast(&backend->cond); + pthread_mutex_unlock(&backend->mutex); + + log_line(backend, + "request reply id=%lu latency=%.3fms handled=%i timed_out=%i failed=%i", + request->id, (request->completed_at - request->queued_at) * 1000.0, + request->handled, request->timed_out, request->failed); + + release_request(backend, request); +} + +static void dispatch_dbus(GLFWx11IMEBackend* backend) +{ + if (!backend->connection) + return; + + dbus_connection_read_write_dispatch(backend->connection, 0); + while (dbus_connection_dispatch(backend->connection) == DBUS_DISPATCH_DATA_REMAINS) + { + } +} + +static void* worker_main(void* data) +{ + GLFWx11IMEBackend* backend = data; + + // This thread owns all D-Bus traffic for the module. GLFW only sees the + // synchronous process_key result and queued events drained on the main thread. + connect_ibus(backend); + + for (;;) + { + Command* command = NULL; + + dispatch_dbus(backend); + + pthread_mutex_lock(&backend->mutex); + if (backend->running && !backend->command_head) + { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 10 * 1000 * 1000; + if (ts.tv_nsec >= 1000000000L) + { + ts.tv_sec++; + ts.tv_nsec -= 1000000000L; + } + pthread_cond_timedwait(&backend->cond, &backend->mutex, &ts); + } + + command = pop_command(backend); + if (!backend->running && !command) + { + pthread_mutex_unlock(&backend->mutex); + break; + } + pthread_mutex_unlock(&backend->mutex); + + if (!command) + continue; + + if (command->type == COMMAND_STOP) + { + free(command); + break; + } + + if (!backend->ready && command->type != COMMAND_STOP) + { + if (command->request) + { + pthread_mutex_lock(&backend->mutex); + command->request->completed = GLFW_TRUE; + command->request->failed = GLFW_TRUE; + command->request->completed_at = now_seconds(backend); + pthread_cond_broadcast(&backend->cond); + pthread_mutex_unlock(&backend->mutex); + release_request(backend, command->request); + } + free(command); + continue; + } + + switch (command->type) + { + case COMMAND_KEY: + process_key_command(backend, command); + break; + case COMMAND_FOCUS_IN: + backend->focused_window = command->window; + call_no_reply(backend, "FocusIn", DBUS_TYPE_INVALID); + break; + case COMMAND_FOCUS_OUT: + if (backend->focused_window == command->window) + backend->focused_window = NULL; + call_no_reply(backend, "FocusOut", DBUS_TYPE_INVALID); + break; + case COMMAND_CURSOR_RECT: + log_line(backend, + "SetCursorLocation final=(%i,%i %ix%i)", + command->x, command->y, command->w, command->h); + call_no_reply(backend, "SetCursorLocation", + DBUS_TYPE_INT32, &command->x, + DBUS_TYPE_INT32, &command->y, + DBUS_TYPE_INT32, &command->w, + DBUS_TYPE_INT32, &command->h, + DBUS_TYPE_INVALID); + break; + case COMMAND_RESET: + call_no_reply(backend, "Reset", DBUS_TYPE_INVALID); + break; + case COMMAND_SET_STATUS: + backend->status = command->status; + call_no_reply(backend, command->status ? "FocusIn" : "FocusOut", + DBUS_TYPE_INVALID); + break; + default: + break; + } + + free(command); + } + + if (backend->connection) + { + dbus_connection_close(backend->connection); + dbus_connection_unref(backend->connection); + backend->connection = NULL; + } + + free(backend->input_context_path); + backend->input_context_path = NULL; + return NULL; +} + +static GLFWx11IMEBackend* backend_create(const GLFWx11IMEHostAPI* host) +{ + GLFWx11IMEBackend* backend = calloc(1, sizeof(GLFWx11IMEBackend)); + const char* timeout; + + if (!backend) + return NULL; + + backend->host = *host; + backend->running = GLFW_TRUE; + backend->timeout_ms = 100.0; + timeout = getenv("GLFW_IBUS_TIMEOUT_MS"); + if (timeout && *timeout) + backend->timeout_ms = atof(timeout); + + pthread_mutex_init(&backend->mutex, NULL); + pthread_cond_init(&backend->cond, NULL); + + if (pthread_create(&backend->thread, NULL, worker_main, backend) != 0) + { + pthread_cond_destroy(&backend->cond); + pthread_mutex_destroy(&backend->mutex); + free(backend); + return NULL; + } + + log_line(backend, "module created timeout=%.3fms", backend->timeout_ms); + return backend; +} + +static void backend_destroy(GLFWx11IMEBackend* backend) +{ + Command* command; + QueuedEvent* event; + Request* request; + + if (!backend) + return; + + command = calloc(1, sizeof(Command)); + if (command) + { + command->type = COMMAND_STOP; + push_command(backend, command); + } + + pthread_mutex_lock(&backend->mutex); + backend->running = GLFW_FALSE; + pthread_cond_signal(&backend->cond); + pthread_mutex_unlock(&backend->mutex); + + pthread_join(backend->thread, NULL); + + while (backend->command_head) + { + command = backend->command_head; + backend->command_head = command->next; + free(command); + } + + while (backend->event_head) + { + event = backend->event_head; + backend->event_head = event->next; + free(event->text); + free(event); + } + + request = backend->recent; + while (request) + { + Request* next = request->next_recent; + request->next_recent = NULL; + release_request(backend, request); + request = next; + } + + pthread_cond_destroy(&backend->cond); + pthread_mutex_destroy(&backend->mutex); + free(backend); +} + +static void enqueue_simple(GLFWx11IMEBackend* backend, + CommandType type, + void* window, + unsigned long x11_window) +{ + Command* command = calloc(1, sizeof(Command)); + if (!command) + return; + + command->type = type; + command->window = window; + command->x11_window = x11_window; + push_command(backend, command); +} + +static void backend_focus_in(GLFWx11IMEBackend* backend, void* window, unsigned long x11_window) +{ + enqueue_simple(backend, COMMAND_FOCUS_IN, window, x11_window); +} + +static void backend_focus_out(GLFWx11IMEBackend* backend, void* window, unsigned long x11_window) +{ + enqueue_simple(backend, COMMAND_FOCUS_OUT, window, x11_window); +} + +static void backend_set_cursor_rect(GLFWx11IMEBackend* backend, + void* window, + int x, int y, int w, int h) +{ + Command* command = calloc(1, sizeof(Command)); + if (!command) + return; + + command->type = COMMAND_CURSOR_RECT; + command->window = window; + command->x = x; + command->y = y; + command->w = w; + command->h = h; + push_command(backend, command); +} + +static void backend_reset(GLFWx11IMEBackend* backend, void* window) +{ + enqueue_simple(backend, COMMAND_RESET, window, 0); +} + +static int backend_process_key(GLFWx11IMEBackend* backend, + const GLFWx11IMEKeyEvent* event, + GLFWx11IMEKeyResult* result) +{ + Command* command; + Request* request; + struct timespec deadline; + int rc = 0; + + request = calloc(1, sizeof(Request)); + command = calloc(1, sizeof(Command)); + if (!request || !command) + { + free(request); + free(command); + return GLFW_FALSE; + } + + pthread_mutex_lock(&backend->mutex); + request->id = ++backend->next_request_id; + request->event = *event; + request->queued_at = now_seconds(backend); + request->refs = 2; + command->type = COMMAND_KEY; + command->window = event->window; + command->x11_window = event->x11_window; + command->request = request; + + if (backend->command_tail) + backend->command_tail->next = command; + else + backend->command_head = command; + backend->command_tail = command; + pthread_cond_signal(&backend->cond); + + clock_gettime(CLOCK_REALTIME, &deadline); + long nsec = (long) (backend->timeout_ms * 1000000.0); + deadline.tv_sec += nsec / 1000000000L; + deadline.tv_nsec += nsec % 1000000000L; + if (deadline.tv_nsec >= 1000000000L) + { + deadline.tv_sec++; + deadline.tv_nsec -= 1000000000L; + } + + while (!request->completed && rc != ETIMEDOUT) + rc = pthread_cond_timedwait(&backend->cond, &backend->mutex, &deadline); + + if (!request->completed) + { + request->timed_out = GLFW_TRUE; + result->timed_out = GLFW_TRUE; + result->handled = GLFW_FALSE; + result->elapsed_ms = (now_seconds(backend) - request->queued_at) * 1000.0; + result->request_id = request->id; + remember_recent(backend, request); + log_line(backend, "request timeout id=%lu latency=%.3fms key_serial=%lu", + request->id, result->elapsed_ms, event->time); + } + else + { + result->timed_out = request->timed_out; + result->handled = request->handled; + result->elapsed_ms = (request->completed_at - request->queued_at) * 1000.0; + result->request_id = request->id; + } + + release_request(backend, request); + pthread_mutex_unlock(&backend->mutex); + + return GLFW_TRUE; +} + +static int backend_get_status(GLFWx11IMEBackend* backend, void* window) +{ + (void) window; + return backend->status; +} + +static void backend_set_status(GLFWx11IMEBackend* backend, void* window, int enabled) +{ + Command* command = calloc(1, sizeof(Command)); + if (!command) + return; + + command->type = COMMAND_SET_STATUS; + command->window = window; + command->status = enabled ? GLFW_TRUE : GLFW_FALSE; + push_command(backend, command); +} + +static void backend_drain_events(GLFWx11IMEBackend* backend) +{ + for (;;) + { + QueuedEvent* event; + + pthread_mutex_lock(&backend->mutex); + event = backend->event_head; + if (event) + { + backend->event_head = event->next; + if (!backend->event_head) + backend->event_tail = NULL; + } + pthread_mutex_unlock(&backend->mutex); + + if (!event) + break; + + log_line(backend, + "event drain type=%i request=%lu timed_out=%i timestamp=%.6f text='%s'", + event->type, event->request_id, event->request_timed_out, + event->timestamp, event->text ? event->text : ""); + + switch (event->type) + { + case EVENT_COMMIT: + if (backend->host.commit_text) + backend->host.commit_text(event->window, event->text ? event->text : ""); + break; + case EVENT_PREEDIT: + if (backend->host.update_preedit) + backend->host.update_preedit(event->window, event->text ? event->text : "", event->caret); + break; + case EVENT_CLEAR_PREEDIT: + if (backend->host.clear_preedit) + backend->host.clear_preedit(event->window); + break; + case EVENT_STATUS: + if (backend->host.status_changed) + backend->host.status_changed(event->window); + break; + } + + free(event->text); + free(event); + } +} + +int glfwGetX11IMEBackend(int abiVersion, + const GLFWx11IMEHostAPI* host, + GLFWx11IMEBackendAPI* backend) +{ + if (abiVersion != GLFW_X11_IME_MODULE_ABI_VERSION || !host || !backend) + return GLFW_FALSE; + + memset(backend, 0, sizeof(*backend)); + backend->create = backend_create; + backend->destroy = backend_destroy; + backend->focus_in = backend_focus_in; + backend->focus_out = backend_focus_out; + backend->set_cursor_rect = backend_set_cursor_rect; + backend->reset = backend_reset; + backend->process_key = backend_process_key; + backend->get_status = backend_get_status; + backend->set_status = backend_set_status; + backend->drain_events = backend_drain_events; + return GLFW_TRUE; +} diff --git a/src/x11_ime_module.c b/src/x11_ime_module.c new file mode 100644 index 0000000000..1ce0f05268 --- /dev/null +++ b/src/x11_ime_module.c @@ -0,0 +1,497 @@ +//======================================================================== +// GLFW 3.5 X11 IME module prototype - www.glfw.org +//======================================================================== + +#include "internal.h" + +#if defined(_GLFW_X11) + +#include "x11_ime_module.h" + +#include +#include +#include + +static void hostLog(const char* message) +{ + if (message && _glfw.x11.imeModule.debug) + fprintf(stderr, "GLFW IME: %s\n", message); +} + +static void hostCommitText(void* handle, const char* utf8) +{ + _GLFWwindow* window = handle; + const char* c = utf8; + + if (!window || !utf8) + return; + + while (*c) + _glfwInputChar(window, _glfwDecodeUTF8(&c), 0, GLFW_TRUE); +} + +static void ensurePreeditBuffers(_GLFWpreedit* preedit, int textCount) +{ + int textBufferCount = preedit->textBufferCount; + + while (textBufferCount < textCount + 1) + textBufferCount = textBufferCount ? textBufferCount * 2 : 8; + + if (textBufferCount != preedit->textBufferCount) + { + unsigned int* text = _glfw_realloc(preedit->text, + sizeof(unsigned int) * textBufferCount); + if (!text) + return; + + preedit->text = text; + preedit->textBufferCount = textBufferCount; + } + + if (preedit->blockSizesBufferCount < 1) + { + int* blocks = _glfw_realloc(preedit->blockSizes, sizeof(int)); + if (!blocks) + return; + + preedit->blockSizes = blocks; + preedit->blockSizesBufferCount = 1; + } +} + +static void hostUpdatePreedit(void* handle, const char* utf8, int caret) +{ + _GLFWwindow* window = handle; + _GLFWpreedit* preedit; + const char* c; + int count = 0; + + if (!window || !utf8) + return; + + c = utf8; + while (*c) + { + _glfwDecodeUTF8(&c); + count++; + } + + preedit = &window->preedit; + ensurePreeditBuffers(preedit, count); + if (count && (!preedit->text || !preedit->blockSizes)) + return; + + c = utf8; + for (int i = 0; i < count; i++) + preedit->text[i] = _glfwDecodeUTF8(&c); + + if (preedit->text) + preedit->text[count] = 0; + + preedit->textCount = count; + preedit->blockSizesCount = count ? 1 : 0; + if (count) + preedit->blockSizes[0] = count; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = (caret >= 0 && caret <= count) ? caret : count; + + _glfwInputPreedit(window); +} + +static void hostClearPreedit(void* handle) +{ + _GLFWwindow* window = handle; + _GLFWpreedit* preedit; + + if (!window) + return; + + preedit = &window->preedit; + preedit->textCount = 0; + preedit->blockSizesCount = 0; + preedit->focusedBlockIndex = 0; + preedit->caretIndex = 0; + + _glfwInputPreedit(window); +} + +static void hostStatusChanged(void* handle) +{ + _GLFWwindow* window = handle; + if (window) + _glfwInputIMEStatus(window); +} + +static double hostGetTime(void) +{ + return _glfwPlatformGetTimerValue() / (double) _glfwPlatformGetTimerFrequency(); +} + +static void hostPostEmptyEvent(void) +{ + // IME modules queue callbacks for the main thread and use this to wake + // glfwWaitEvents without exposing their worker-thread file descriptors. + _glfwPostEmptyEventX11(); +} + +GLFWbool _glfwLoadIMEModuleX11(void) +{ + const char* path = getenv("GLFW_IM_MODULE"); + const char* debug = getenv("GLFW_IME_DEBUG"); + GLFWx11IMEHostAPI host; + GLFWx11IMEBackendAPI api; + PFN_glfwGetX11IMEBackend getBackend; + + if (!path || !*path) + return GLFW_FALSE; + + _glfw.x11.imeModule.debug = debug && *debug && strcmp(debug, "0") != 0; + + _glfw.x11.imeModule.handle = _glfwPlatformLoadModule(path); + if (!_glfw.x11.imeModule.handle) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: Failed to load IME module %s", path); + return GLFW_FALSE; + } + + getBackend = (PFN_glfwGetX11IMEBackend) + _glfwPlatformGetModuleSymbol(_glfw.x11.imeModule.handle, + "glfwGetX11IMEBackend"); + if (!getBackend) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: IME module does not export glfwGetX11IMEBackend"); + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); + return GLFW_FALSE; + } + + memset(&host, 0, sizeof(host)); + host.commit_text = hostCommitText; + host.update_preedit = hostUpdatePreedit; + host.clear_preedit = hostClearPreedit; + host.status_changed = hostStatusChanged; + host.get_time = hostGetTime; + host.post_empty_event = hostPostEmptyEvent; + host.log = hostLog; + + memset(&api, 0, sizeof(api)); + if (!getBackend(GLFW_X11_IME_MODULE_ABI_VERSION, &host, &api) || + !api.create || !api.destroy || !api.process_key || !api.drain_events) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: IME module rejected ABI version %i", + GLFW_X11_IME_MODULE_ABI_VERSION); + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); + return GLFW_FALSE; + } + + _glfw.x11.imeModule.api = api; + _glfw.x11.imeModule.backend = api.create(&host); + if (!_glfw.x11.imeModule.backend) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: IME module failed to create backend"); + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); + return GLFW_FALSE; + } + + return GLFW_TRUE; +} + +void _glfwUnloadIMEModuleX11(void) +{ + if (_glfw.x11.imeModule.backend && _glfw.x11.imeModule.api.destroy) + _glfw.x11.imeModule.api.destroy(_glfw.x11.imeModule.backend); + + if (_glfw.x11.imeModule.handle) + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); +} + +GLFWbool _glfwHasIMEModuleX11(void) +{ + return _glfw.x11.imeModule.backend != NULL; +} + +void _glfwDrainIMEModuleX11(void) +{ + if (_glfwHasIMEModuleX11()) + _glfw.x11.imeModule.api.drain_events(_glfw.x11.imeModule.backend); +} + +static void sendCachedCursorRect(_GLFWwindow* window, const char* reason) +{ + if (!_glfwHasIMEModuleX11() || + !_glfw.x11.imeModule.api.set_cursor_rect) + { + return; + } + + if (!window->x11.imeCursorRectValid) + { + if (_glfw.x11.imeModule.debug) + { + fprintf(stderr, + "GLFW IME: skip SetCursorLocation reason=%s valid=0 pending=1 original=(%i,%i %ix%i)\n", + reason, + window->preedit.cursorPosX, + window->preedit.cursorPosY, + window->preedit.cursorWidth, + window->preedit.cursorHeight); + } + window->x11.imeCursorRectPending = GLFW_TRUE; + return; + } + + if (!window->x11.imeCursorRectSent && _glfw.x11.imeModule.debug) + { + fprintf(stderr, + "GLFW IME: first SetCursorLocation reason=%s original=(%i,%i %ix%i) window_root=(%i,%i) final=(%i,%i %ix%i)\n", + reason, + window->x11.imeCursorX, + window->x11.imeCursorY, + window->x11.imeCursorWidth, + window->x11.imeCursorHeight, + window->x11.imeWindowRootX, + window->x11.imeWindowRootY, + window->x11.imeCursorRootX, + window->x11.imeCursorRootY, + window->x11.imeCursorWidth, + window->x11.imeCursorHeight); + } + + _glfw.x11.imeModule.api.set_cursor_rect(_glfw.x11.imeModule.backend, + window, + window->x11.imeCursorRootX, + window->x11.imeCursorRootY, + window->x11.imeCursorWidth, + window->x11.imeCursorHeight); + + window->x11.imeCursorRectSent = GLFW_TRUE; + window->x11.imeCursorRectPending = GLFW_FALSE; +} + +void _glfwRefreshCursorRectIMEModuleX11(_GLFWwindow* window, const char* reason) +{ + Window child; + XWindowAttributes attributes; + int glfwX = 0; + int glfwY = 0; + int windowRootX = 0; + int windowRootY = 0; + int cursorRootX = window->preedit.cursorPosX; + int cursorRootY = window->preedit.cursorPosY; + const int localX = window->preedit.cursorPosX; + const int localY = window->preedit.cursorPosY; + const int localW = window->preedit.cursorWidth; + const int localH = window->preedit.cursorHeight; + const Bool windowTranslated = + XTranslateCoordinates(_glfw.x11.display, + window->x11.handle, + _glfw.x11.root, + 0, 0, + &windowRootX, &windowRootY, + &child); + const Bool cursorTranslated = + XTranslateCoordinates(_glfw.x11.display, + window->x11.handle, + _glfw.x11.root, + localX, localY, + &cursorRootX, &cursorRootY, + &child); + const Status attributesStatus = + XGetWindowAttributes(_glfw.x11.display, window->x11.handle, &attributes); + const int screenWidth = DisplayWidth(_glfw.x11.display, _glfw.x11.screen); + const int screenHeight = DisplayHeight(_glfw.x11.display, _glfw.x11.screen); + const GLFWbool onScreen = + cursorRootX > -screenWidth && + cursorRootY > -screenHeight && + cursorRootX < screenWidth * 2 && + cursorRootY < screenHeight * 2; + const GLFWbool looksInitialized = + window->x11.imeNormalKeySeen || + windowRootX != 0 || + windowRootY != 0 || + window->x11.imeCursorRectRetries > 0; + const GLFWbool plausible = + windowTranslated && + cursorTranslated && + attributesStatus && + attributes.map_state == IsViewable && + onScreen && + looksInitialized; + + _glfwGetWindowPosX11(window, &glfwX, &glfwY); + + window->x11.imeCursorX = localX; + window->x11.imeCursorY = localY; + window->x11.imeCursorWidth = localW; + window->x11.imeCursorHeight = localH; + window->x11.imeWindowRootX = windowRootX; + window->x11.imeWindowRootY = windowRootY; + window->x11.imeCursorRootX = cursorRootX; + window->x11.imeCursorRootY = cursorRootY; + window->x11.imeCursorRectValid = plausible; + window->x11.imeCursorRectPending = !plausible; + + if (_glfw.x11.imeModule.debug) + { + fprintf(stderr, + "GLFW IME: cursor translate reason=%s source=0x%lx root=0x%lx window_ret=%i cursor_ret=%i window_root=(%i,%i) cursor_root=(%i,%i) glfw_pos=(%i,%i) local=(%i,%i %ix%i) map_state=%i normal_key_seen=%i retries=%i plausible=%i\n", + reason, + (unsigned long) window->x11.handle, + (unsigned long) _glfw.x11.root, + windowTranslated ? 1 : 0, + cursorTranslated ? 1 : 0, + windowRootX, windowRootY, + cursorRootX, cursorRootY, + glfwX, glfwY, + localX, localY, localW, localH, + attributesStatus ? attributes.map_state : -1, + window->x11.imeNormalKeySeen ? 1 : 0, + window->x11.imeCursorRectRetries, + plausible ? 1 : 0); + } + + if (!plausible && window->x11.imeCursorRectRetries < 2) + { + window->x11.imeCursorRectRetries++; + _glfwPostEmptyEventX11(); + } +} + +void _glfwRefreshPendingCursorRectsIMEModuleX11(const char* reason) +{ + if (!_glfwHasIMEModuleX11()) + return; + + for (_GLFWwindow* window = _glfw.windowListHead; window; window = window->next) + { + if (window->x11.imeCursorRectPending) + { + _glfwRefreshCursorRectIMEModuleX11(window, reason); + sendCachedCursorRect(window, reason); + } + } +} + +void _glfwNotifyNormalKeyIMEModuleX11(_GLFWwindow* window) +{ + if (!_glfwHasIMEModuleX11()) + return; + + window->x11.imeNormalKeySeen = GLFW_TRUE; + if (window->x11.imeCursorRectPending) + { + _glfwRefreshCursorRectIMEModuleX11(window, "after-normal-key"); + sendCachedCursorRect(window, "after-normal-key"); + } +} + +GLFWbool _glfwProcessKeyIMEModuleX11(_GLFWwindow* window, + unsigned int keycode, + unsigned int keysym, + unsigned int state, + int action, + int mods, + unsigned long time) +{ + GLFWx11IMEKeyEvent event; + GLFWx11IMEKeyResult result; + + if (!_glfwHasIMEModuleX11()) + return GLFW_FALSE; + + _glfwRefreshCursorRectIMEModuleX11(window, "before-key"); + sendCachedCursorRect(window, "before-key"); + + memset(&event, 0, sizeof(event)); + event.window = window; + event.x11_window = window->x11.handle; + event.keycode = keycode; + event.keysym = keysym; + event.state = state; + event.action = action; + event.mods = mods; + event.time = time; + event.cursor_rect_valid = window->x11.imeCursorRectValid; + event.cursor_x = window->x11.imeCursorX; + event.cursor_y = window->x11.imeCursorY; + event.cursor_width = window->x11.imeCursorWidth; + event.cursor_height = window->x11.imeCursorHeight; + event.cursor_root_x = window->x11.imeCursorRootX; + event.cursor_root_y = window->x11.imeCursorRootY; + + memset(&result, 0, sizeof(result)); + if (!_glfw.x11.imeModule.api.process_key(_glfw.x11.imeModule.backend, + &event, &result)) + { + return GLFW_FALSE; + } + + _glfwDrainIMEModuleX11(); + return result.handled ? GLFW_TRUE : GLFW_FALSE; +} + +void _glfwFocusInIMEModuleX11(_GLFWwindow* window) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.focus_in) + { + window->x11.imeLogNextKey = GLFW_TRUE; + _glfw.x11.imeModule.api.focus_in(_glfw.x11.imeModule.backend, + window, window->x11.handle); + _glfwRefreshCursorRectIMEModuleX11(window, "focus-in"); + sendCachedCursorRect(window, "focus-in"); + } +} + +void _glfwFocusOutIMEModuleX11(_GLFWwindow* window) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.focus_out) + _glfw.x11.imeModule.api.focus_out(_glfw.x11.imeModule.backend, + window, window->x11.handle); +} + +void _glfwSetCursorRectIMEModuleX11(_GLFWwindow* window, int x, int y, int w, int h) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.set_cursor_rect) + { + // Applications provide client-area coordinates via the public IME API. + // The IBus X11 panel needs root-window coordinates for candidate + // placement, so the conversion stays local to this experimental path. + window->preedit.cursorPosX = x; + window->preedit.cursorPosY = y; + window->preedit.cursorWidth = w; + window->preedit.cursorHeight = h; + _glfwRefreshCursorRectIMEModuleX11(window, "cursor-update"); + sendCachedCursorRect(window, "cursor-update"); + } +} + +void _glfwResetIMEModuleX11(_GLFWwindow* window) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.reset) + _glfw.x11.imeModule.api.reset(_glfw.x11.imeModule.backend, window); +} + +void _glfwSetStatusIMEModuleX11(_GLFWwindow* window, int active) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.set_status) + _glfw.x11.imeModule.api.set_status(_glfw.x11.imeModule.backend, + window, active); +} + +int _glfwGetStatusIMEModuleX11(_GLFWwindow* window) +{ + if (_glfwHasIMEModuleX11() && _glfw.x11.imeModule.api.get_status) + return _glfw.x11.imeModule.api.get_status(_glfw.x11.imeModule.backend, window); + + return GLFW_FALSE; +} + +#endif // _GLFW_X11 diff --git a/src/x11_ime_module.h b/src/x11_ime_module.h new file mode 100644 index 0000000000..4273acbdce --- /dev/null +++ b/src/x11_ime_module.h @@ -0,0 +1,97 @@ +//======================================================================== +// GLFW 3.5 X11 IME module prototype - www.glfw.org +//------------------------------------------------------------------------ +// This is an experimental internal ABI for dynamically loaded X11 IME +// modules. It is intentionally not part of the public GLFW API. +//======================================================================== + +#ifndef _glfw3_x11_ime_module_h_ +#define _glfw3_x11_ime_module_h_ + +/* + * The experimental IME module ABI currently references _GLFWwindow + * directly, so we need the internal declaration here. + * + * A future public/stable module ABI should avoid exposing internal + * GLFW types and pass the required state explicitly instead. + */ +#include "internal.h" + +#define GLFW_X11_IME_MODULE_ABI_VERSION 1 + +typedef struct GLFWx11IMEBackend GLFWx11IMEBackend; + +typedef struct GLFWx11IMEKeyEvent +{ + void* window; + unsigned long x11_window; + unsigned int keycode; + unsigned int keysym; + unsigned int state; + int action; + int mods; + unsigned long time; + int cursor_rect_valid; + int cursor_x, cursor_y, cursor_width, cursor_height; + int cursor_root_x, cursor_root_y; +} GLFWx11IMEKeyEvent; + +typedef struct GLFWx11IMEKeyResult +{ + int handled; + int timed_out; + double elapsed_ms; + unsigned long request_id; +} GLFWx11IMEKeyResult; + +typedef struct GLFWx11IMEHostAPI +{ + void (*commit_text)(void* window, const char* utf8); + void (*update_preedit)(void* window, const char* utf8, int caret); + void (*clear_preedit)(void* window); + void (*status_changed)(void* window); + double (*get_time)(void); + void (*post_empty_event)(void); + void (*log)(const char* message); +} GLFWx11IMEHostAPI; + +typedef struct GLFWx11IMEBackendAPI +{ + GLFWx11IMEBackend* (*create)(const GLFWx11IMEHostAPI* host); + void (*destroy)(GLFWx11IMEBackend* backend); + void (*focus_in)(GLFWx11IMEBackend* backend, void* window, unsigned long x11_window); + void (*focus_out)(GLFWx11IMEBackend* backend, void* window, unsigned long x11_window); + void (*set_cursor_rect)(GLFWx11IMEBackend* backend, void* window, int x, int y, int w, int h); + void (*reset)(GLFWx11IMEBackend* backend, void* window); + int (*process_key)(GLFWx11IMEBackend* backend, + const GLFWx11IMEKeyEvent* event, + GLFWx11IMEKeyResult* result); + int (*get_status)(GLFWx11IMEBackend* backend, void* window); + void (*set_status)(GLFWx11IMEBackend* backend, void* window, int enabled); + void (*drain_events)(GLFWx11IMEBackend* backend); +} GLFWx11IMEBackendAPI; + +typedef int (* PFN_glfwGetX11IMEBackend)(int,const GLFWx11IMEHostAPI*,GLFWx11IMEBackendAPI*); + +int _glfwLoadIMEModuleX11(void); +void _glfwUnloadIMEModuleX11(void); +int _glfwHasIMEModuleX11(void); +void _glfwDrainIMEModuleX11(void); +int _glfwProcessKeyIMEModuleX11(_GLFWwindow* window, + unsigned int keycode, + unsigned int keysym, + unsigned int state, + int action, + int mods, + unsigned long time); +void _glfwFocusInIMEModuleX11(_GLFWwindow* window); +void _glfwFocusOutIMEModuleX11(_GLFWwindow* window); +void _glfwSetCursorRectIMEModuleX11(_GLFWwindow* window, int x, int y, int w, int h); +void _glfwRefreshCursorRectIMEModuleX11(_GLFWwindow* window, const char* reason); +void _glfwRefreshPendingCursorRectsIMEModuleX11(const char* reason); +void _glfwNotifyNormalKeyIMEModuleX11(_GLFWwindow* window); +void _glfwResetIMEModuleX11(_GLFWwindow* window); +void _glfwSetStatusIMEModuleX11(_GLFWwindow* window, int active); +int _glfwGetStatusIMEModuleX11(_GLFWwindow* window); + +#endif // _glfw3_x11_ime_module_h_ diff --git a/src/x11_init.c b/src/x11_init.c index 720eed2216..c4ca12a3c8 100644 --- a/src/x11_init.c +++ b/src/x11_init.c @@ -1553,7 +1553,9 @@ int _glfwInitX11(void) _glfw.x11.helperWindowHandle = createHelperWindow(); _glfw.x11.hiddenCursorHandle = createHiddenCursor(); - if (XSupportsLocale() && _glfw.x11.xlib.utf8) + _glfwLoadIMEModuleX11(); + + if (!_glfwHasIMEModuleX11() && XSupportsLocale() && _glfw.x11.xlib.utf8) { XSetLocaleModifiers(""); @@ -1591,10 +1593,13 @@ void _glfwTerminateX11(void) _glfw_free(_glfw.x11.primarySelectionString); _glfw_free(_glfw.x11.clipboardString); - XUnregisterIMInstantiateCallback(_glfw.x11.display, - NULL, NULL, NULL, - inputMethodInstantiateCallback, - NULL); + if (!_glfwHasIMEModuleX11()) + { + XUnregisterIMInstantiateCallback(_glfw.x11.display, + NULL, NULL, NULL, + inputMethodInstantiateCallback, + NULL); + } if (_glfw.x11.im) { @@ -1602,6 +1607,8 @@ void _glfwTerminateX11(void) _glfw.x11.im = NULL; } + _glfwUnloadIMEModuleX11(); + if (_glfw.x11.display) { XCloseDisplay(_glfw.x11.display); diff --git a/src/x11_platform.h b/src/x11_platform.h index 2800105257..5643f178f4 100644 --- a/src/x11_platform.h +++ b/src/x11_platform.h @@ -50,6 +50,8 @@ // The Shape extension provides custom window shapes #include +#include "x11_ime_module.h" + #define GLX_VENDOR 1 #define GLX_RGBA_BIT 0x00000001 #define GLX_WINDOW_BIT 0x00000001 @@ -565,6 +567,15 @@ typedef struct _GLFWwindowX11 XIMCallback statusDrawCallback; int imeFocus; + GLFWbool imeCursorRectValid; + GLFWbool imeCursorRectSent; + GLFWbool imeLogNextKey; + GLFWbool imeNormalKeySeen; + GLFWbool imeCursorRectPending; + int imeCursorRectRetries; + int imeCursorX, imeCursorY, imeCursorWidth, imeCursorHeight; + int imeWindowRootX, imeWindowRootY; + int imeCursorRootX, imeCursorRootY; } _GLFWwindowX11; // X11-specific global data @@ -898,6 +909,13 @@ typedef struct _GLFWlibraryX11 PFN_XShapeQueryVersion QueryVersion; PFN_XShapeCombineMask ShapeCombineMask; } xshape; + + struct { + void* handle; + GLFWx11IMEBackend* backend; + GLFWx11IMEBackendAPI api; + GLFWbool debug; + } imeModule; } _GLFWlibraryX11; // X11-specific per-monitor data diff --git a/src/x11_window.c b/src/x11_window.c index 0915be9744..59999bbe2a 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -244,6 +244,16 @@ static int translateKey(int scancode) return _glfw.x11.keycodes[scancode]; } +static KeySym getKeySym(XKeyEvent* event, int keycode) +{ + if (_glfw.x11.xkb.available) + return XkbKeycodeToKeysym(_glfw.x11.display, keycode, _glfw.x11.xkb.group, 0); + + KeySym keysym = NoSymbol; + XLookupString(event, NULL, 0, &keysym, NULL); + return keysym; +} + // Sends an EWMH or ICCCM event to the window manager // static void sendEventToWM(_GLFWwindow* window, Atom type, @@ -1357,12 +1367,14 @@ static void processEvent(XEvent *event) { int keycode = 0; Bool filtered = False; + const GLFWbool imeModuleActive = _glfwHasIMEModuleX11(); // HACK: Save scancode as some IMs clear the field in XFilterEvent if (event->type == KeyPress || event->type == KeyRelease) keycode = event->xkey.keycode; - filtered = XFilterEvent(event, None); + if (!imeModuleActive) + filtered = XFilterEvent(event, None); if (_glfw.x11.randr.available) { @@ -1455,6 +1467,20 @@ static void processEvent(XEvent *event) const int key = translateKey(keycode); const int mods = translateState(event->xkey.state); const int plain = !(mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT)); + const KeySym keysym = getKeySym(&event->xkey, keycode); + + if (_glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) keysym, + event->xkey.state, GLFW_PRESS, mods, + event->xkey.time)) + { + if (keycode) + _glfwInputKey(window, key, keycode, GLFW_PRESS, mods); + + return; + } + + if (imeModuleActive && window->x11.imeLogNextKey) + window->x11.imeLogNextKey = GLFW_FALSE; if (window->x11.ic) { @@ -1519,6 +1545,9 @@ static void processEvent(XEvent *event) _glfwInputChar(window, codepoint, mods, plain); } + if (imeModuleActive) + _glfwNotifyNormalKeyIMEModuleX11(window); + return; } @@ -1526,6 +1555,7 @@ static void processEvent(XEvent *event) { const int key = translateKey(keycode); const int mods = translateState(event->xkey.state); + const KeySym keysym = getKeySym(&event->xkey, keycode); if (!_glfw.x11.xkb.detectable) { @@ -1559,6 +1589,14 @@ static void processEvent(XEvent *event) } } + if (_glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) keysym, + event->xkey.state, GLFW_RELEASE, mods, + event->xkey.time)) + { + _glfwInputKey(window, key, keycode, GLFW_RELEASE, mods); + return; + } + _glfwInputKey(window, key, keycode, GLFW_RELEASE, mods); return; } @@ -1966,6 +2004,8 @@ static void processEvent(XEvent *event) { XSetICFocus(window->x11.ic); } + else if (!window->textInputFocusExplicit || window->textInputFocus) + _glfwFocusInIMEModuleX11(window); _glfwInputWindowFocus(window, GLFW_TRUE); return; @@ -1988,6 +2028,8 @@ static void processEvent(XEvent *event) if (window->x11.ic) XUnsetICFocus(window->x11.ic); + else + _glfwFocusOutIMEModuleX11(window); if (window->monitor && window->autoIconify) _glfwIconifyWindowX11(window); @@ -2273,6 +2315,9 @@ GLFWbool _glfwCreateWindowX11(_GLFWwindow* window, if (wndconfig->mousePassthrough) _glfwSetWindowMousePassthroughX11(window, GLFW_TRUE); + if (_glfwHasIMEModuleX11()) + window->x11.imeLogNextKey = GLFW_TRUE; + if (window->monitor) { _glfwShowWindowX11(window); @@ -2309,6 +2354,8 @@ void _glfwDestroyWindowX11(_GLFWwindow* window) XDestroyIC(window->x11.ic); window->x11.ic = NULL; } + else + _glfwFocusOutIMEModuleX11(window); if (window->context.destroy) window->context.destroy(window); @@ -3056,6 +3103,8 @@ GLFWbool _glfwRawMouseMotionSupportedX11(void) void _glfwPollEventsX11(void) { drainEmptyEvents(); + _glfwRefreshPendingCursorRectsIMEModuleX11("event-drain"); + _glfwDrainIMEModuleX11(); #if defined(GLFW_BUILD_LINUX_JOYSTICK) if (_glfw.joysticksInitialized) @@ -3068,6 +3117,8 @@ void _glfwPollEventsX11(void) XEvent event; XNextEvent(_glfw.x11.display, &event); processEvent(&event); + _glfwRefreshPendingCursorRectsIMEModuleX11("after-event"); + _glfwDrainIMEModuleX11(); } _GLFWwindow* window = _glfw.x11.disabledCursorWindow; @@ -3086,6 +3137,8 @@ void _glfwPollEventsX11(void) } XFlush(_glfw.x11.display); + _glfwRefreshPendingCursorRectsIMEModuleX11("poll-end"); + _glfwDrainIMEModuleX11(); } void _glfwWaitEventsX11(void) @@ -3366,6 +3419,16 @@ void _glfwUpdatePreeditCursorRectangleX11(_GLFWwindow* window) XPoint spot; _GLFWpreedit* preedit = &window->preedit; + if (_glfwHasIMEModuleX11()) + { + _glfwSetCursorRectIMEModuleX11(window, + preedit->cursorPosX, + preedit->cursorPosY, + preedit->cursorWidth, + preedit->cursorHeight); + return; + } + if (!window->x11.ic) return; @@ -3386,6 +3449,12 @@ void _glfwResetPreeditTextX11(_GLFWwindow* window) XVaNestedList preedit_attr; char* result; + if (_glfwHasIMEModuleX11()) + { + _glfwResetIMEModuleX11(window); + return; + } + if (!ic) return; @@ -3419,6 +3488,12 @@ void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) { XIC ic = window->x11.ic; + if (_glfwHasIMEModuleX11()) + { + _glfwSetStatusIMEModuleX11(window, active); + return; + } + if (!ic) return; @@ -3452,6 +3527,9 @@ void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused) int _glfwGetIMEStatusX11(_GLFWwindow* window) { + if (_glfwHasIMEModuleX11()) + return _glfwGetStatusIMEModuleX11(window); + if (!window->x11.ic) return GLFW_FALSE; From 52ec41883b57540fe3ae8364f2605901eaea7b0b Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sat, 20 Jun 2026 10:35:47 +0900 Subject: [PATCH 08/12] X11: Fix IME module key event suppression Keep XFilterEvent as the suppressor for the XIM path, but do not call it before the experimental IME module sees KeyPress events. When the module reports a key as handled, return before normal GLFW key and character processing. This gives the IME module path the same kind of duplicate-input suppression that XFilterEvent provides for the XIM path, while keeping the XIM behavior unchanged. This effectively extends the duplicate-input suppression addressed by PR #1972 to the experimental IME module path. --- src/x11_window.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/x11_window.c b/src/x11_window.c index 59999bbe2a..2bfe396ffa 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -1374,7 +1374,11 @@ static void processEvent(XEvent *event) keycode = event->xkey.keycode; if (!imeModuleActive) + { filtered = XFilterEvent(event, None); + if (filtered) + return; + } if (_glfw.x11.randr.available) { @@ -1468,20 +1472,22 @@ static void processEvent(XEvent *event) const int mods = translateState(event->xkey.state); const int plain = !(mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT)); const KeySym keysym = getKeySym(&event->xkey, keycode); + GLFWbool moduleHandled = GLFW_FALSE; - if (_glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) keysym, - event->xkey.state, GLFW_PRESS, mods, - event->xkey.time)) + if (imeModuleActive) { - if (keycode) - _glfwInputKey(window, key, keycode, GLFW_PRESS, mods); - - return; + moduleHandled = + _glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) keysym, + event->xkey.state, GLFW_PRESS, mods, + event->xkey.time); } if (imeModuleActive && window->x11.imeLogNextKey) window->x11.imeLogNextKey = GLFW_FALSE; + if (moduleHandled) + return; + if (window->x11.ic) { // HACK: Do not report the key press events duplicated by XIM From 86048ee181fccda740c38a55a488a35d3de8d69c Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Wed, 24 Jun 2026 14:34:22 +0900 Subject: [PATCH 09/12] X11: Implement text input focus for IME module Map glfwSetTextInputFocus to the experimental X11 IME module path. When text input focus is enabled for a focused X11 window, send FocusIn to the module so IBus resumes routing text input. When it is disabled, reset composition and send FocusOut, keeping GLFW window focus separate from the text input focus abstraction. Also gate the module KeyPress path on GLFW's text input focus state so FocusOut actually stops routing input through IBus. Applications that never opt into explicit text input focus keep the legacy behavior. Document the prototype mapping without adding a new module ABI entry. --- docs/ime-ibus-prototype.md | 10 ++++++++++ src/x11_window.c | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md index d902043a14..3c3c9086d2 100644 --- a/docs/ime-ibus-prototype.md +++ b/docs/ime-ibus-prototype.md @@ -193,6 +193,16 @@ The bridge tracks whether a valid cursor rectangle has been translated. It does not send `SetCursorLocation` until a valid rectangle exists. It resends the last valid rectangle on focus and before key processing. +### Text Input Focus + +`glfwSetTextInputFocus(window, GLFW_TRUE)` maps to IBus `FocusIn` when the X11 +window is focused. If the X11 window is not focused, the request is reflected +by GLFW state and the next X11 `FocusIn` event activates the module. + +`glfwSetTextInputFocus(window, GLFW_FALSE)` maps to IBus `Reset` followed by +`FocusOut`. This disables text input routing for the window while keeping the +window focus state separate from the text input focus abstraction. + ### IME Enable And Disable Behavior The prototype has minimal IME status support. diff --git a/src/x11_window.c b/src/x11_window.c index 2bfe396ffa..4c93a80f6e 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -1472,9 +1472,11 @@ static void processEvent(XEvent *event) const int mods = translateState(event->xkey.state); const int plain = !(mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT)); const KeySym keysym = getKeySym(&event->xkey, keycode); + const GLFWbool textInputFocused = + !window->textInputFocusExplicit || window->textInputFocus; GLFWbool moduleHandled = GLFW_FALSE; - if (imeModuleActive) + if (imeModuleActive && textInputFocused) { moduleHandled = _glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) keysym, @@ -3517,6 +3519,22 @@ void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused) { XIC ic = window->x11.ic; + if (_glfwHasIMEModuleX11()) + { + if (focused) + { + if (_glfwWindowFocusedX11(window)) + _glfwFocusInIMEModuleX11(window); + } + else + { + _glfwResetPreeditTextX11(window); + _glfwFocusOutIMEModuleX11(window); + } + + return; + } + if (!ic) return; From c291c3782c413edeb75c970621a2657e29ab6779 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Wed, 24 Jun 2026 14:47:02 +0900 Subject: [PATCH 10/12] X11: Map IBus preedit attributes to GLFW blocks Parse IBus UpdatePreeditText cursor position, visibility and text attributes in the experimental X11 IME module. The module now converts IBus attribute ranges into GLFW preedit block sizes, picks a focused block from highlighted attributes or the caret, and passes the caret index through to the GLFW main-thread preedit update. This keeps D-Bus and IBus details inside the module while allowing applications to draw richer preedit state. Bump the experimental module ABI because the host preedit callback now carries block metadata. --- docs/ime-ibus-prototype.md | 7 +- src/x11_ibus_module.c | 421 ++++++++++++++++++++++++++++++++++--- src/x11_ime_module.c | 39 +++- src/x11_ime_module.h | 9 +- 4 files changed, 435 insertions(+), 41 deletions(-) diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md index 3c3c9086d2..13e21aa0ac 100644 --- a/docs/ime-ibus-prototype.md +++ b/docs/ime-ibus-prototype.md @@ -172,9 +172,10 @@ production IBus backend design. Preedit updates from IBus are received through `UpdatePreeditText`, queued by the worker and drained on the GLFW main thread. -The prototype maps preedit text to a single focused block. This is sufficient -for testing text flow and candidate positioning, but it is not a complete IBus -attribute implementation. +The prototype maps IBus preedit text, caret position and attribute ranges to +GLFW preedit text, block sizes, focused block and caret index. The block +mapping is intentionally conservative because IBus engines differ in which +attributes they use to mark the active segment. ### Commit Support diff --git a/src/x11_ibus_module.c b/src/x11_ibus_module.c index fb0badfbff..de4a1c663f 100644 --- a/src/x11_ibus_module.c +++ b/src/x11_ibus_module.c @@ -37,6 +37,12 @@ enum IBUS_RELEASE_MASK = 1 << 30 }; +enum +{ + IBUS_ATTR_TYPE_UNDERLINE = 1, + IBUS_ATTR_UNDERLINE_SINGLE = 1 +}; + typedef enum CommandType { COMMAND_KEY, @@ -87,12 +93,32 @@ typedef struct QueuedEvent void* window; char* text; int caret; + int* block_sizes; + int block_count; + int focused_block; unsigned long request_id; int request_timed_out; double timestamp; struct QueuedEvent* next; } QueuedEvent; +typedef struct PreeditBlock +{ + int start; + int end; + int focused; +} PreeditBlock; + +typedef struct PreeditInfo +{ + const char* text; + int caret; + int visible; + int* block_sizes; + int block_count; + int focused_block; +} PreeditInfo; + struct GLFWx11IMEBackend { GLFWx11IMEHostAPI host; @@ -243,7 +269,10 @@ static void enqueue_event(GLFWx11IMEBackend* backend, EventType type, void* window, const char* text, - int caret) + int caret, + const int* block_sizes, + int block_count, + int focused_block) { QueuedEvent* event = calloc(1, sizeof(QueuedEvent)); unsigned long request_id; @@ -267,6 +296,17 @@ static void enqueue_event(GLFWx11IMEBackend* backend, backend->active_window ? backend->active_window : backend->focused_window; event->text = text ? xstrdup(text) : NULL; + if (block_sizes && block_count > 0) + { + event->block_sizes = calloc((size_t) block_count, sizeof(int)); + if (event->block_sizes) + { + memcpy(event->block_sizes, block_sizes, + sizeof(int) * (size_t) block_count); + event->block_count = block_count; + } + } + event->focused_block = focused_block; event->caret = caret; event->request_id = request_id; event->request_timed_out = request ? request->timed_out : 0; @@ -281,9 +321,10 @@ static void enqueue_event(GLFWx11IMEBackend* backend, pthread_mutex_unlock(&backend->mutex); log_line(backend, - "event queued type=%i request=%lu timed_out=%i timestamp=%.6f text='%s'", + "event queued type=%i request=%lu timed_out=%i timestamp=%.6f caret=%i blocks=%i focused=%i text='%s'", type, event->request_id, event->request_timed_out, - event->timestamp, text ? text : ""); + event->timestamp, event->caret, event->block_count, + event->focused_block, text ? text : ""); if (backend->host.post_empty_event) backend->host.post_empty_event(); @@ -350,35 +391,341 @@ static int call_no_reply(GLFWx11IMEBackend* backend, const char* method, int fir return GLFW_TRUE; } -static const char* parse_ibus_text(DBusMessage* message) +static int compare_ints(const void* a, const void* b) +{ + const int ia = *(const int*) a; + const int ib = *(const int*) b; + return (ia > ib) - (ia < ib); +} + +static int utf8_count(const char* text) { - DBusMessageIter iter, variant, structure; + int count = 0; + const unsigned char* p = (const unsigned char*) text; + + while (p && *p) + { + if ((*p & 0xc0) != 0x80) + count++; + p++; + } + + return count; +} + +static int normalize_ibus_index(const char* text, int char_count, int index) +{ + const char* p = text; + int chars = 0; + + if (index <= 0) + return 0; + + if (index <= char_count) + return index; + + for (int byte = 0; p && *p; byte++) + { + if (byte == index) + return chars; + if (((unsigned char) *p & 0xc0) != 0x80) + chars++; + p++; + } + + return char_count; +} + +static void append_preedit_block(PreeditBlock** blocks, + int* count, + int* capacity, + int start, + int end, + int focused) +{ + if (start >= end) + return; + + if (*count == *capacity) + { + const int new_capacity = *capacity ? *capacity * 2 : 8; + PreeditBlock* new_blocks = + realloc(*blocks, sizeof(PreeditBlock) * (size_t) new_capacity); + if (!new_blocks) + return; + + *blocks = new_blocks; + *capacity = new_capacity; + } + + (*blocks)[*count].start = start; + (*blocks)[*count].end = end; + (*blocks)[*count].focused = focused; + (*count)++; +} + +static void parse_ibus_attribute(DBusMessageIter* iter, + const char* text, + int char_count, + PreeditBlock** blocks, + int* count, + int* capacity) +{ + DBusMessageIter structure; const char* id = NULL; - const char* text = NULL; + dbus_uint32_t type = 0, value = 0, start = 0, end = 0; - dbus_message_iter_init(message, &iter); - if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) - return NULL; + if (dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_VARIANT) + { + DBusMessageIter variant; + dbus_message_iter_recurse(iter, &variant); + parse_ibus_attribute(&variant, text, char_count, blocks, count, capacity); + return; + } + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRUCT) + return; + + dbus_message_iter_recurse(iter, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return; + + dbus_message_iter_get_basic(&structure, &id); + if (!id || strcmp(id, "IBusAttribute") != 0) + return; + + if (!dbus_message_iter_next(&structure) || + !dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_UINT32) + { + return; + } + dbus_message_iter_get_basic(&structure, &type); + + if (!dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_UINT32) + { + return; + } + dbus_message_iter_get_basic(&structure, &value); + + if (!dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_UINT32) + { + return; + } + dbus_message_iter_get_basic(&structure, &start); + + if (!dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_UINT32) + { + return; + } + dbus_message_iter_get_basic(&structure, &end); + + append_preedit_block(blocks, count, capacity, + normalize_ibus_index(text, char_count, (int) start), + normalize_ibus_index(text, char_count, (int) end), + type != IBUS_ATTR_TYPE_UNDERLINE || + value != IBUS_ATTR_UNDERLINE_SINGLE); +} + +static void parse_ibus_attr_list(DBusMessageIter* iter, + const char* text, + int char_count, + PreeditBlock** blocks, + int* count, + int* capacity) +{ + DBusMessageIter structure; + const char* id = NULL; + + if (dbus_message_iter_get_arg_type(iter) == DBUS_TYPE_VARIANT) + { + DBusMessageIter variant; + dbus_message_iter_recurse(iter, &variant); + parse_ibus_attr_list(&variant, text, char_count, blocks, count, capacity); + return; + } + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_STRUCT) + return; + + dbus_message_iter_recurse(iter, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return; + + dbus_message_iter_get_basic(&structure, &id); + if (!id || strcmp(id, "IBusAttrList") != 0) + return; + + while (dbus_message_iter_next(&structure)) + { + if (dbus_message_iter_get_arg_type(&structure) == DBUS_TYPE_ARRAY) + { + DBusMessageIter array; + dbus_message_iter_recurse(&structure, &array); + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) + { + parse_ibus_attribute(&array, text, char_count, + blocks, count, capacity); + dbus_message_iter_next(&array); + } + } + } +} + +static void build_preedit_blocks(PreeditInfo* info, + PreeditBlock* attrs, + int attr_count) +{ + int boundary_count = 0; + int focused_start = -1; + const int text_count = utf8_count(info->text); + int* boundaries; + + info->focused_block = 0; + + if (!text_count) + return; + + boundaries = calloc((size_t) attr_count * 2 + 2, sizeof(int)); + if (!boundaries) + return; + + boundaries[boundary_count++] = 0; + boundaries[boundary_count++] = text_count; + + for (int i = 0; i < attr_count; i++) + { + int start = attrs[i].start; + int end = attrs[i].end; + + if (start < 0) + start = 0; + if (end > text_count) + end = text_count; + if (start >= end) + continue; + + boundaries[boundary_count++] = start; + boundaries[boundary_count++] = end; + + if (attrs[i].focused && focused_start < 0) + focused_start = start; + } + + qsort(boundaries, (size_t) boundary_count, sizeof(int), compare_ints); + + info->block_sizes = calloc((size_t) boundary_count, sizeof(int)); + if (!info->block_sizes) + { + free(boundaries); + return; + } + + int previous = boundaries[0]; + for (int i = 1; i < boundary_count; i++) + { + const int current = boundaries[i]; + if (current == previous) + continue; + + info->block_sizes[info->block_count] = current - previous; + + if (focused_start >= previous && focused_start < current) + info->focused_block = info->block_count; + else if (focused_start < 0 && + info->caret >= previous && + (info->caret < current || current == text_count)) + { + info->focused_block = info->block_count; + } - dbus_message_iter_recurse(&iter, &variant); + info->block_count++; + previous = current; + } + + free(boundaries); +} + +static int parse_ibus_text_variant(DBusMessageIter* iter, + PreeditInfo* info, + PreeditBlock** attrs, + int* attr_count, + int* attr_capacity) +{ + DBusMessageIter variant, structure; + const char* id = NULL; + + if (dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_VARIANT) + return GLFW_FALSE; + + dbus_message_iter_recurse(iter, &variant); if (dbus_message_iter_get_arg_type(&variant) != DBUS_TYPE_STRUCT) - return NULL; + return GLFW_FALSE; dbus_message_iter_recurse(&variant, &structure); if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) - return NULL; + return GLFW_FALSE; dbus_message_iter_get_basic(&structure, &id); if (!id || strcmp(id, "IBusText") != 0) - return NULL; + return GLFW_FALSE; dbus_message_iter_next(&structure); dbus_message_iter_next(&structure); if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) - return NULL; + return GLFW_FALSE; + + dbus_message_iter_get_basic(&structure, &info->text); + + while (dbus_message_iter_next(&structure)) + parse_ibus_attr_list(&structure, info->text, utf8_count(info->text), + attrs, attr_count, attr_capacity); + + return GLFW_TRUE; +} + +static int parse_ibus_text(DBusMessage* message, PreeditInfo* info) +{ + DBusMessageIter iter; + PreeditBlock* attrs = NULL; + int attr_count = 0; + int attr_capacity = 0; + + memset(info, 0, sizeof(*info)); + info->caret = -1; + info->visible = GLFW_TRUE; + + dbus_message_iter_init(message, &iter); + if (!parse_ibus_text_variant(&iter, info, &attrs, &attr_count, &attr_capacity)) + { + free(attrs); + return GLFW_FALSE; + } - dbus_message_iter_get_basic(&structure, &text); - return text; + if (dbus_message_iter_next(&iter) && + dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_UINT32) + { + dbus_uint32_t caret = 0; + dbus_message_iter_get_basic(&iter, &caret); + info->caret = normalize_ibus_index(info->text, + utf8_count(info->text), + (int) caret); + } + + if (dbus_message_iter_next(&iter) && + dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_BOOLEAN) + { + dbus_bool_t visible = 1; + dbus_message_iter_get_basic(&iter, &visible); + info->visible = visible ? GLFW_TRUE : GLFW_FALSE; + } + + build_preedit_blocks(info, attrs, attr_count); + free(attrs); + return GLFW_TRUE; } static DBusHandlerResult dbus_filter(DBusConnection* connection, @@ -386,40 +733,51 @@ static DBusHandlerResult dbus_filter(DBusConnection* connection, void* data) { GLFWx11IMEBackend* backend = data; - const char* text; + PreeditInfo info; (void) connection; if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "CommitText")) { - text = parse_ibus_text(message); - enqueue_event(backend, EVENT_COMMIT, NULL, text ? text : "", -1); + if (parse_ibus_text(message, &info)) + enqueue_event(backend, EVENT_COMMIT, NULL, info.text ? info.text : "", -1, + NULL, 0, 0); + free(info.block_sizes); return DBUS_HANDLER_RESULT_HANDLED; } if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "UpdatePreeditText")) { - text = parse_ibus_text(message); - enqueue_event(backend, EVENT_PREEDIT, NULL, text ? text : "", -1); + if (parse_ibus_text(message, &info)) + { + if (info.visible) + enqueue_event(backend, EVENT_PREEDIT, NULL, info.text ? info.text : "", + info.caret, info.block_sizes, info.block_count, + info.focused_block); + else + enqueue_event(backend, EVENT_CLEAR_PREEDIT, NULL, NULL, -1, + NULL, 0, 0); + } + free(info.block_sizes); return DBUS_HANDLER_RESULT_HANDLED; } if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "HidePreeditText")) { - enqueue_event(backend, EVENT_CLEAR_PREEDIT, NULL, NULL, -1); + enqueue_event(backend, EVENT_CLEAR_PREEDIT, NULL, NULL, -1, NULL, 0, 0); return DBUS_HANDLER_RESULT_HANDLED; } if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "Enabled")) { backend->status = GLFW_TRUE; - enqueue_event(backend, EVENT_STATUS, NULL, NULL, -1); + enqueue_event(backend, EVENT_STATUS, NULL, NULL, -1, NULL, 0, 0); return DBUS_HANDLER_RESULT_HANDLED; } if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "Disabled")) { backend->status = GLFW_FALSE; - enqueue_event(backend, EVENT_STATUS, NULL, NULL, -1); + enqueue_event(backend, EVENT_STATUS, NULL, NULL, -1, NULL, 0, 0); return DBUS_HANDLER_RESULT_HANDLED; } @@ -1050,9 +1408,10 @@ static void backend_drain_events(GLFWx11IMEBackend* backend) break; log_line(backend, - "event drain type=%i request=%lu timed_out=%i timestamp=%.6f text='%s'", + "event drain type=%i request=%lu timed_out=%i timestamp=%.6f caret=%i blocks=%i focused=%i text='%s'", event->type, event->request_id, event->request_timed_out, - event->timestamp, event->text ? event->text : ""); + event->timestamp, event->caret, event->block_count, + event->focused_block, event->text ? event->text : ""); switch (event->type) { @@ -1062,7 +1421,14 @@ static void backend_drain_events(GLFWx11IMEBackend* backend) break; case EVENT_PREEDIT: if (backend->host.update_preedit) - backend->host.update_preedit(event->window, event->text ? event->text : "", event->caret); + { + backend->host.update_preedit(event->window, + event->text ? event->text : "", + event->caret, + event->block_sizes, + event->block_count, + event->focused_block); + } break; case EVENT_CLEAR_PREEDIT: if (backend->host.clear_preedit) @@ -1075,6 +1441,7 @@ static void backend_drain_events(GLFWx11IMEBackend* backend) } free(event->text); + free(event->block_sizes); free(event); } } diff --git a/src/x11_ime_module.c b/src/x11_ime_module.c index 1ce0f05268..ec948a3437 100644 --- a/src/x11_ime_module.c +++ b/src/x11_ime_module.c @@ -30,9 +30,10 @@ static void hostCommitText(void* handle, const char* utf8) _glfwInputChar(window, _glfwDecodeUTF8(&c), 0, GLFW_TRUE); } -static void ensurePreeditBuffers(_GLFWpreedit* preedit, int textCount) +static void ensurePreeditBuffers(_GLFWpreedit* preedit, int textCount, int blockCount) { int textBufferCount = preedit->textBufferCount; + int blockBufferCount = preedit->blockSizesBufferCount; while (textBufferCount < textCount + 1) textBufferCount = textBufferCount ? textBufferCount * 2 : 8; @@ -48,18 +49,27 @@ static void ensurePreeditBuffers(_GLFWpreedit* preedit, int textCount) preedit->textBufferCount = textBufferCount; } - if (preedit->blockSizesBufferCount < 1) + while (blockBufferCount < blockCount) + blockBufferCount = blockBufferCount ? blockBufferCount * 2 : 8; + + if (blockBufferCount != preedit->blockSizesBufferCount) { - int* blocks = _glfw_realloc(preedit->blockSizes, sizeof(int)); + int* blocks = _glfw_realloc(preedit->blockSizes, + sizeof(int) * blockBufferCount); if (!blocks) return; preedit->blockSizes = blocks; - preedit->blockSizesBufferCount = 1; + preedit->blockSizesBufferCount = blockBufferCount; } } -static void hostUpdatePreedit(void* handle, const char* utf8, int caret) +static void hostUpdatePreedit(void* handle, + const char* utf8, + int caret, + const int* blockSizes, + int blockCount, + int focusedBlock) { _GLFWwindow* window = handle; _GLFWpreedit* preedit; @@ -77,7 +87,10 @@ static void hostUpdatePreedit(void* handle, const char* utf8, int caret) } preedit = &window->preedit; - ensurePreeditBuffers(preedit, count); + if (!blockSizes || blockCount <= 0) + blockCount = count ? 1 : 0; + + ensurePreeditBuffers(preedit, count, blockCount); if (count && (!preedit->text || !preedit->blockSizes)) return; @@ -89,10 +102,18 @@ static void hostUpdatePreedit(void* handle, const char* utf8, int caret) preedit->text[count] = 0; preedit->textCount = count; - preedit->blockSizesCount = count ? 1 : 0; - if (count) + preedit->blockSizesCount = blockCount; + if (blockSizes && blockCount > 0) + { + for (int i = 0; i < blockCount; i++) + preedit->blockSizes[i] = blockSizes[i]; + } + else if (count) preedit->blockSizes[0] = count; - preedit->focusedBlockIndex = 0; + + if (focusedBlock < 0 || focusedBlock >= blockCount) + focusedBlock = 0; + preedit->focusedBlockIndex = focusedBlock; preedit->caretIndex = (caret >= 0 && caret <= count) ? caret : count; _glfwInputPreedit(window); diff --git a/src/x11_ime_module.h b/src/x11_ime_module.h index 4273acbdce..461b740a27 100644 --- a/src/x11_ime_module.h +++ b/src/x11_ime_module.h @@ -17,7 +17,7 @@ */ #include "internal.h" -#define GLFW_X11_IME_MODULE_ABI_VERSION 1 +#define GLFW_X11_IME_MODULE_ABI_VERSION 2 typedef struct GLFWx11IMEBackend GLFWx11IMEBackend; @@ -47,7 +47,12 @@ typedef struct GLFWx11IMEKeyResult typedef struct GLFWx11IMEHostAPI { void (*commit_text)(void* window, const char* utf8); - void (*update_preedit)(void* window, const char* utf8, int caret); + void (*update_preedit)(void* window, + const char* utf8, + int caret, + const int* block_sizes, + int block_count, + int focused_block); void (*clear_preedit)(void* window); void (*status_changed)(void* window); double (*get_time)(void); From 8de69c2eed5b7c05ae501b7520959b83634d4106 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Wed, 24 Jun 2026 15:57:17 +0900 Subject: [PATCH 11/12] Docs: Update X11 IBus prototype status Document the current state of the experimental X11 IBus module after adding text input focus gating and richer preedit handling. The prototype now maps IBus caret and attribute ranges to GLFW preedit state, gates ProcessKeyEvent on explicit text input focus, and is close to usable for application testing while still remaining experimental. --- docs/ime-ibus-prototype.md | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md index 13e21aa0ac..c29fe6a307 100644 --- a/docs/ime-ibus-prototype.md +++ b/docs/ime-ibus-prototype.md @@ -18,6 +18,8 @@ The prototype is meant to answer these questions: - Do IBus replies or text signals arrive late enough to cause duplicated text? - Is candidate window positioning practical with the existing cursor rectangle API? +- Can the existing GLFW preedit model represent useful IBus preedit state, + including caret position and focused text blocks? ## Architecture @@ -119,6 +121,8 @@ The module uses these IBus input context methods and signals: - `FocusOut` - `SetCursorLocation` - `Reset` +- `Enabled` +- `Disabled` The module currently uses `SetCursorLocation`, not `SetCursorLocationRelative`. @@ -177,6 +181,11 @@ GLFW preedit text, block sizes, focused block and caret index. The block mapping is intentionally conservative because IBus engines differ in which attributes they use to mark the active segment. +This has been tested with normal IBus preedit flow and now behaves well enough +for practical application-side preedit drawing in the prototype. It is still a +mapping from IBus attributes to GLFW's simpler block model, not a lossless +exposure of every IBus text attribute. + ### Commit Support Committed text from `CommitText` is queued by the worker and emitted through @@ -204,6 +213,11 @@ by GLFW state and the next X11 `FocusIn` event activates the module. `FocusOut`. This disables text input routing for the window while keeping the window focus state separate from the text input focus abstraction. +The X11 key path also checks GLFW's text input focus state before sending +`ProcessKeyEvent` to the module. This is required because IBus `FocusOut` is +asynchronous and does not by itself prevent GLFW from continuing to route key +events through IBus. + ### IME Enable And Disable Behavior The prototype has minimal IME status support. @@ -250,6 +264,7 @@ Each queued and drained IME event logs: - attributed request id - whether the attributed request had timed out - timestamp +- caret index, block count and focused block for preedit events - text, when present IBus signals do not include the originating `ProcessKeyEvent` request. The @@ -286,11 +301,15 @@ Late `CommitText` events are queued and logged with the best available request attribution. Because IBus does not identify the originating request, this attribution is not guaranteed to be exact. -### Candidate And Surrounding Text Completeness +### Candidate, Attribute And Surrounding Text Completeness -The prototype does not implement lookup-table/candidate-list parsing, -surrounding text, or full IBus preedit attributes. These would be needed for a -more complete backend. +The prototype does not implement lookup-table/candidate-list parsing or +surrounding text. + +IBus preedit attributes are mapped to GLFW block sizes and a focused block, but +the mapping is intentionally lossy. It is enough for useful preedit display, +but it does not expose underline style, foreground/background color or every +IBus text attribute to applications. ### Restart And Recovery @@ -306,10 +325,17 @@ The prototype demonstrates that the architecture is technically feasible: - worker-thread communication can be kept behind explicit queues - IBus/Fcitx5 preedit and commit paths can be integrated with the existing IME architecture +- IBus preedit text, caret position and focused block information can be mapped + to GLFW's existing preedit callback model - candidate window positioning can be handled without changing the public IME API +- explicit text input focus can be made to stop routing X11 keys through IBus + +The current prototype is no longer only a proof of concept for drawing basic +preedit text. It is close to usable for application testing on X11 with IBus or +Fcitx5's IBus compatibility layer. -However, this is still a research prototype. It is not currently recommended -for upstream submission. +However, this is still an experimental backend. It is not currently recommended +for upstream submission as-is. The remaining decision point is semantic reliability: late replies and late text signals after key-processing timeouts need more real-world measurement before an From 536fbb437887e1e29e588e9f0a4205e99cc0bf64 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Wed, 24 Jun 2026 16:52:35 +0900 Subject: [PATCH 12/12] X11: Add bundled IBus compatibility modes Add two switches for game/application compatibility testing. GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS makes GLFW_IME control GLFW's text input focus path by default, and also acts as the runtime override environment variable. This lets applications that already toggle GLFW_IME use the more portable text-input-focus semantics without code changes. glfwGetInputMode(GLFW_IME) keeps reporting the native IME status until text input focus has been explicitly set for the window, preserving the old query behavior for applications that never use the compatibility mode. GLFW_EMBED_IBUS_MODULE builds the IBus backend into the X11 GLFW library. The embedded backend is used when GLFW_IM_MODULE is not set, while GLFW_IM_MODULE can still override it with an external module. --- CMakeLists.txt | 6 +++- docs/ime-ibus-prototype.md | 21 +++++++++++++ src/CMakeLists.txt | 16 ++++++++++ src/input.c | 29 ++++++++++++++++++ src/x11_ime_module.c | 60 ++++++++++++++++++++++++++------------ src/x11_ime_module.h | 18 ++++++------ 6 files changed, 121 insertions(+), 29 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 063533c4ed..450ed036f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,8 @@ option(GLFW_BUILD_EXAMPLES "Build the GLFW example programs" ${GLFW_STANDALONE}) option(GLFW_BUILD_TESTS "Build the GLFW test programs" ${GLFW_STANDALONE}) option(GLFW_BUILD_DOCS "Build the GLFW documentation" ON) option(GLFW_INSTALL "Generate installation target" ON) +option(GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS + "Map GLFW_IME input mode to text input focus by default" OFF) include(GNUInstallDirs) include(CMakeDependentOption) @@ -28,6 +30,9 @@ cmake_dependent_option(GLFW_BUILD_WIN32 "Build support for Win32" ON "WIN32" OFF cmake_dependent_option(GLFW_BUILD_COCOA "Build support for Cocoa" ON "APPLE" OFF) cmake_dependent_option(GLFW_BUILD_X11 "Build support for X11" ON "UNIX;NOT APPLE" OFF) cmake_dependent_option(GLFW_BUILD_WAYLAND "Build support for Wayland" ON "UNIX;NOT APPLE" OFF) +cmake_dependent_option(GLFW_EMBED_IBUS_MODULE + "Embed the experimental X11 IBus IME module into GLFW" + OFF "UNIX;NOT APPLE;GLFW_BUILD_X11" OFF) cmake_dependent_option(GLFW_USE_HYBRID_HPG "Force use of high-performance GPU on hybrid systems" OFF "WIN32" OFF) @@ -133,4 +138,3 @@ if (GLFW_INSTALL) set_target_properties(uninstall PROPERTIES FOLDER "GLFW3") endif() endif() - diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md index c29fe6a307..331f5bf13b 100644 --- a/docs/ime-ibus-prototype.md +++ b/docs/ime-ibus-prototype.md @@ -36,6 +36,11 @@ For example: GLFW_IM_MODULE=/path/to/glfw-ibus.so ./application ``` +For experimentation with prebuilt GLFW binaries, the same IBus backend can also +be embedded into GLFW with the `GLFW_EMBED_IBUS_MODULE` CMake option. In that +mode, GLFW uses the embedded backend when `GLFW_IM_MODULE` is not set, while +still allowing `GLFW_IM_MODULE` to override it. + ### GLFW Core GLFW core remains unaware of IBus, Fcitx5 and D-Bus. The X11 backend only knows @@ -73,6 +78,11 @@ does not add D-Bus file descriptors to the GLFW event loop. The prototype module is built as `glfw-ibus.so` when the `dbus-1` development package is available. +If `GLFW_EMBED_IBUS_MODULE` is enabled, the same module source is also compiled +into the GLFW library and `dbus-1` becomes a GLFW build dependency. This is +only an experiment to remove setup friction for local or redistributed test +builds. + The module owns: - IBus address discovery @@ -218,6 +228,17 @@ The X11 key path also checks GLFW's text input focus state before sending asynchronous and does not by itself prevent GLFW from continuing to route key events through IBus. +For application compatibility experiments, `GLFW_IME` can be remapped to this +text input focus model. Set `GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS` to a non-zero +value, or build GLFW with `GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS` enabled. In this +mode, `glfwSetInputMode(window, GLFW_IME, value)` calls the text input focus path +instead of the native platform IME-status path. + +`glfwGetInputMode(window, GLFW_IME)` still returns the native platform IME +status until text input focus has been explicitly set for the window. After +that, it returns the explicit text input focus state. This preserves the old +query behavior for applications that never use the remapping path. + ### IME Enable And Disable Behavior The prototype has minimal IME status support. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5b7a0d63f9..8feea278e1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -141,6 +141,10 @@ target_include_directories(glfw PRIVATE "${GLFW_BINARY_DIR}/src") target_link_libraries(glfw PRIVATE Threads::Threads) +if (GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS) + target_compile_definitions(glfw PRIVATE _GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS) +endif() + if (GLFW_BUILD_WIN32) list(APPEND glfw_PKG_LIBS "-lgdi32") endif() @@ -218,7 +222,19 @@ if (GLFW_BUILD_X11) if (CMAKE_SYSTEM_NAME STREQUAL "Linux") include(FindPkgConfig) pkg_check_modules(DBUS1 dbus-1) + if (GLFW_EMBED_IBUS_MODULE AND NOT DBUS1_FOUND) + message(FATAL_ERROR "D-Bus development package is required for embedded glfw-ibus") + endif() if (DBUS1_FOUND) + if (GLFW_EMBED_IBUS_MODULE) + target_sources(glfw PRIVATE x11_ibus_module.c) + target_compile_definitions(glfw PRIVATE _GLFW_EMBED_IBUS_MODULE) + target_include_directories(glfw PRIVATE ${DBUS1_INCLUDE_DIRS}) + target_link_libraries(glfw PRIVATE ${DBUS1_LIBRARIES}) + target_compile_options(glfw PRIVATE ${DBUS1_CFLAGS_OTHER}) + target_link_directories(glfw PRIVATE ${DBUS1_LIBRARY_DIRS}) + endif() + add_library(glfw-ibus MODULE x11_ibus_module.c x11_ime_module.h) set_target_properties(glfw-ibus PROPERTIES PREFIX "" diff --git a/src/input.c b/src/input.c index 23391ad5f5..cac4590902 100644 --- a/src/input.c +++ b/src/input.c @@ -593,6 +593,20 @@ void _glfwCenterCursorInContentArea(_GLFWwindow* window) _glfw.platform.setCursorPos(window, width / 2.0, height / 2.0); } +static GLFWbool imeModeControlsTextInputFocus(void) +{ + const char* value = getenv("GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS"); + + if (value && *value) + return strcmp(value, "0") != 0; + +#if defined(_GLFW_IME_MODE_AS_TEXT_INPUT_FOCUS) + return GLFW_TRUE; +#else + return GLFW_FALSE; +#endif +} + ////////////////////////////////////////////////////////////////////////// ////// GLFW public API ////// @@ -620,6 +634,12 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) case GLFW_UNLIMITED_MOUSE_BUTTONS: return window->disableMouseButtonLimit; case GLFW_IME: + if (imeModeControlsTextInputFocus() && + window->textInputFocusExplicit) + { + return window->textInputFocus; + } + return _glfw.platform.getIMEStatus(window); } @@ -737,6 +757,15 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) case GLFW_IME: { + if (imeModeControlsTextInputFocus()) + { + value = value ? GLFW_TRUE : GLFW_FALSE; + window->textInputFocusExplicit = GLFW_TRUE; + window->textInputFocus = value; + _glfw.platform.setTextInputFocus(window, value); + return; + } + _glfw.platform.setIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); return; } diff --git a/src/x11_ime_module.c b/src/x11_ime_module.c index ec948a3437..34702722bf 100644 --- a/src/x11_ime_module.c +++ b/src/x11_ime_module.c @@ -12,6 +12,10 @@ #include #include +#if defined(_GLFW_EMBED_IBUS_MODULE) +extern int glfwGetX11IMEBackend(int,const GLFWx11IMEHostAPI*,GLFWx11IMEBackendAPI*); +#endif + static void hostLog(const char* message) { if (message && _glfw.x11.imeModule.debug) @@ -163,30 +167,46 @@ GLFWbool _glfwLoadIMEModuleX11(void) GLFWx11IMEBackendAPI api; PFN_glfwGetX11IMEBackend getBackend; - if (!path || !*path) +#if defined(_GLFW_EMBED_IBUS_MODULE) + getBackend = glfwGetX11IMEBackend; +#else + getBackend = NULL; +#endif + + if ((!path || !*path) && !getBackend) return GLFW_FALSE; _glfw.x11.imeModule.debug = debug && *debug && strcmp(debug, "0") != 0; - _glfw.x11.imeModule.handle = _glfwPlatformLoadModule(path); - if (!_glfw.x11.imeModule.handle) + if (path && *path) { - _glfwInputError(GLFW_PLATFORM_ERROR, - "X11: Failed to load IME module %s", path); - return GLFW_FALSE; - } + _glfw.x11.imeModule.handle = _glfwPlatformLoadModule(path); + if (!_glfw.x11.imeModule.handle) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: Failed to load IME module %s", path); + return GLFW_FALSE; + } - getBackend = (PFN_glfwGetX11IMEBackend) - _glfwPlatformGetModuleSymbol(_glfw.x11.imeModule.handle, - "glfwGetX11IMEBackend"); - if (!getBackend) - { - _glfwInputError(GLFW_PLATFORM_ERROR, - "X11: IME module does not export glfwGetX11IMEBackend"); - _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); - memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); - return GLFW_FALSE; + getBackend = (PFN_glfwGetX11IMEBackend) + _glfwPlatformGetModuleSymbol(_glfw.x11.imeModule.handle, + "glfwGetX11IMEBackend"); + if (!getBackend) + { + _glfwInputError(GLFW_PLATFORM_ERROR, + "X11: IME module does not export glfwGetX11IMEBackend"); + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); + return GLFW_FALSE; + } + + if (_glfw.x11.imeModule.debug) + fprintf(stderr, "GLFW IME: loaded external module %s\n", path); } +#if defined(_GLFW_EMBED_IBUS_MODULE) + else if (_glfw.x11.imeModule.debug) + fprintf(stderr, "GLFW IME: using embedded IBus module\n"); +#endif memset(&host, 0, sizeof(host)); host.commit_text = hostCommitText; @@ -204,7 +224,8 @@ GLFWbool _glfwLoadIMEModuleX11(void) _glfwInputError(GLFW_PLATFORM_ERROR, "X11: IME module rejected ABI version %i", GLFW_X11_IME_MODULE_ABI_VERSION); - _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + if (_glfw.x11.imeModule.handle) + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); return GLFW_FALSE; } @@ -215,7 +236,8 @@ GLFWbool _glfwLoadIMEModuleX11(void) { _glfwInputError(GLFW_PLATFORM_ERROR, "X11: IME module failed to create backend"); - _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); + if (_glfw.x11.imeModule.handle) + _glfwPlatformFreeModule(_glfw.x11.imeModule.handle); memset(&_glfw.x11.imeModule, 0, sizeof(_glfw.x11.imeModule)); return GLFW_FALSE; } diff --git a/src/x11_ime_module.h b/src/x11_ime_module.h index 461b740a27..c0da57e296 100644 --- a/src/x11_ime_module.h +++ b/src/x11_ime_module.h @@ -8,15 +8,6 @@ #ifndef _glfw3_x11_ime_module_h_ #define _glfw3_x11_ime_module_h_ -/* - * The experimental IME module ABI currently references _GLFWwindow - * directly, so we need the internal declaration here. - * - * A future public/stable module ABI should avoid exposing internal - * GLFW types and pass the required state explicitly instead. - */ -#include "internal.h" - #define GLFW_X11_IME_MODULE_ABI_VERSION 2 typedef struct GLFWx11IMEBackend GLFWx11IMEBackend; @@ -78,6 +69,15 @@ typedef struct GLFWx11IMEBackendAPI typedef int (* PFN_glfwGetX11IMEBackend)(int,const GLFWx11IMEHostAPI*,GLFWx11IMEBackendAPI*); +/* + * The experimental IME module ABI currently references _GLFWwindow in GLFW's + * internal X11 helpers, so we need the internal declaration here. + * + * A future public/stable module ABI should avoid exposing internal GLFW types + * and pass the required state explicitly instead. + */ +#include "internal.h" + int _glfwLoadIMEModuleX11(void); void _glfwUnloadIMEModuleX11(void); int _glfwHasIMEModuleX11(void);