From 7aab90b60f25154893fb3fb8bba07492a7492dce Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sat, 20 Jun 2026 10:35:33 +0900 Subject: [PATCH 1/8] X11: Add experimental IBus IME module prototype Add a private X11 IME module ABI and an experimental glfw-ibus.so backend for feasibility testing. The module owns D-Bus communication, IBus input contexts and its worker thread, while GLFW drains queued IME events on the main thread without modifying the event loop architecture. The prototype handles ProcessKeyEvent, CommitText, UpdatePreeditText, focus changes, reset, IME status signals and cursor location updates. Key events are passed to IBus with the keysym translated from the X key event so printable shifted keys such as Shift+1 reach IBus as the expected key value. IBus preedit text, caret position and attribute ranges are mapped onto GLFW preedit text, block sizes, focused block and caret index. Candidate window placement uses SetCursorLocation with X11 root-window coordinates, including the startup/focus ordering workaround for the first candidate window. This is a research prototype, not an upstream-ready feature. It keeps GLFW_IME_DEBUG instrumentation and documentation for measuring latency, timeouts and late IBus replies or text signals. --- CMakeLists.txt | 13 +- docs/ime-ibus-prototype.md | 317 ++++++++ src/CMakeLists.txt | 37 +- src/x11_ibus_module.c | 1468 ++++++++++++++++++++++++++++++++++++ src/x11_ime_module.c | 653 ++++++++++++++++ src/x11_ime_module.h | 102 +++ src/x11_init.c | 17 +- src/x11_platform.h | 18 + src/x11_window.c | 91 ++- 9 files changed, 2707 insertions(+), 9 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/CMakeLists.txt b/CMakeLists.txt index 063533c4ed..ced37f885a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,15 @@ option(GLFW_INSTALL "Generate installation target" ON) include(GNUInstallDirs) include(CMakeDependentOption) +set(GLFW_IME_MODULE_INSTALL_DIR "${CMAKE_INSTALL_LIBDIR}/glfw" CACHE PATH + "Install directory for experimental X11 IME modules") +if (IS_ABSOLUTE "${GLFW_IME_MODULE_INSTALL_DIR}") + set(GLFW_IME_MODULE_INSTALL_FULL_DIR "${GLFW_IME_MODULE_INSTALL_DIR}") +else() + set(GLFW_IME_MODULE_INSTALL_FULL_DIR + "${CMAKE_INSTALL_PREFIX}/${GLFW_IME_MODULE_INSTALL_DIR}") +endif() + if (GLFW_USE_OSMESA) message(FATAL_ERROR "GLFW_USE_OSMESA has been removed; set the GLFW_PLATFORM init hint") endif() @@ -28,6 +37,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_INSTALL_IME_MODULES + "Install experimental X11 IME modules" + OFF "UNIX;NOT APPLE;GLFW_BUILD_X11;GLFW_INSTALL" OFF) cmake_dependent_option(GLFW_USE_HYBRID_HPG "Force use of high-performance GPU on hybrid systems" OFF "WIN32" OFF) @@ -133,4 +145,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 new file mode 100644 index 0000000000..d678ff78ea --- /dev/null +++ b/docs/ime-ibus-prototype.md @@ -0,0 +1,317 @@ +# 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 +``` + +The standalone IBus IME module is not installed by default. Configure with +`-DGLFW_INSTALL_IME_MODULES=ON` to install `glfw-ibus.so`. The default install +location is `${CMAKE_INSTALL_LIBDIR}/glfw` relative to the install prefix, +typically `lib/glfw` under that prefix. For example, with the default +`/usr/local` prefix this is usually `/usr/local/lib/glfw`. + +When `GLFW_IM_MODULE` does not contain a `/`, GLFW also searches this default +module directory. The `glfw-` prefix and platform module suffix may be omitted, +so `GLFW_IM_MODULE=ibus` loads the installed `glfw-ibus.so` module. Full paths +continue to be loaded as specified. + +### 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 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 + +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..ae08ada32e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -46,7 +46,11 @@ endif() if (GLFW_BUILD_X11) target_compile_definitions(glfw PRIVATE _GLFW_X11) - target_sources(glfw PRIVATE x11_platform.h x11_init.c + target_compile_definitions(glfw PRIVATE + "_GLFW_X11_IME_MODULE_DIR=\"${GLFW_IME_MODULE_INSTALL_FULL_DIR}\"" + "_GLFW_X11_IME_MODULE_SUFFIX=\"${CMAKE_SHARED_MODULE_SUFFIX}\"") + 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 +217,36 @@ 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 (GLFW_INSTALL_IME_MODULES AND NOT DBUS1_FOUND) + message(FATAL_ERROR "D-Bus development package is required for installing X11 IME modules") + endif() + 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}) + + if (GLFW_INSTALL_IME_MODULES) + install(TARGETS glfw-ibus + LIBRARY DESTINATION "${GLFW_IME_MODULE_INSTALL_DIR}") + endif() + else() + message(STATUS "D-Bus development package not found; skipping glfw-ibus prototype module") + endif() + endif() endif() if (UNIX AND NOT APPLE) @@ -345,4 +379,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..de4a1c663f --- /dev/null +++ b/src/x11_ibus_module.c @@ -0,0 +1,1468 @@ +#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 +}; + +enum +{ + IBUS_ATTR_TYPE_UNDERLINE = 1, + IBUS_ATTR_UNDERLINE_SINGLE = 1 +}; + +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; + 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; + 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, + const int* block_sizes, + int block_count, + int focused_block) +{ + 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; + 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; + 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 caret=%i blocks=%i focused=%i text='%s'", + type, event->request_id, event->request_timed_out, + event->timestamp, event->caret, event->block_count, + event->focused_block, 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 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) +{ + 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; + dbus_uint32_t type = 0, value = 0, start = 0, end = 0; + + 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; + } + + 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 GLFW_FALSE; + + dbus_message_iter_recurse(&variant, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + return GLFW_FALSE; + + dbus_message_iter_get_basic(&structure, &id); + if (!id || strcmp(id, "IBusText") != 0) + 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 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; + } + + 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, + DBusMessage* message, + void* data) +{ + GLFWx11IMEBackend* backend = data; + PreeditInfo info; + (void) connection; + + if (dbus_message_is_signal(message, IBUS_INPUT_INTERFACE, "CommitText")) + { + 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")) + { + 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, 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, 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, NULL, 0, 0); + 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 caret=%i blocks=%i focused=%i text='%s'", + event->type, event->request_id, event->request_timed_out, + event->timestamp, event->caret, event->block_count, + event->focused_block, 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, + event->block_sizes, + event->block_count, + event->focused_block); + } + 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->block_sizes); + 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..8e65710f7a --- /dev/null +++ b/src/x11_ime_module.c @@ -0,0 +1,653 @@ +//======================================================================== +// 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 + +#if !defined(_GLFW_X11_IME_MODULE_DIR) +#define _GLFW_X11_IME_MODULE_DIR "" +#endif + +#if !defined(_GLFW_X11_IME_MODULE_SUFFIX) +#define _GLFW_X11_IME_MODULE_SUFFIX ".so" +#endif + +static void hostLog(const char* message) +{ + if (message && _glfw.x11.imeModule.debug) + fprintf(stderr, "GLFW IME: %s\n", message); +} + +static GLFWbool hasPathSeparator(const char* path) +{ + return strchr(path, '/') != NULL; +} + +static GLFWbool hasSuffix(const char* string, const char* suffix) +{ + const size_t stringLength = strlen(string); + const size_t suffixLength = strlen(suffix); + + if (suffixLength > stringLength) + return GLFW_FALSE; + + return strcmp(string + stringLength - suffixLength, suffix) == 0; +} + +static char* makeIMEModulePath(const char* directory, + const char* prefix, + const char* name, + const char* suffix) +{ + const size_t directoryLength = strlen(directory); + const size_t prefixLength = strlen(prefix); + const size_t nameLength = strlen(name); + const size_t suffixLength = strlen(suffix); + const size_t length = directoryLength + 1 + prefixLength + nameLength + + suffixLength; + char* path = _glfw_calloc(length + 1, 1); + + if (!path) + return NULL; + + snprintf(path, length + 1, "%s/%s%s%s", directory, prefix, name, suffix); + return path; +} + +static void* loadIMEModulePath(const char* path, char** loadedPath) +{ + void* handle = _glfwPlatformLoadModule(path); + + if (!handle) + return NULL; + + *loadedPath = _glfw_strdup(path); + return handle; +} + +static void* loadIMEModuleName(const char* name, char** loadedPath) +{ + void* handle; + const GLFWbool hasModuleSuffix = hasSuffix(name, _GLFW_X11_IME_MODULE_SUFFIX); + const GLFWbool hasModulePrefix = strncmp(name, "glfw-", 5) == 0; + const char* directory = _GLFW_X11_IME_MODULE_DIR; + char* path; + + *loadedPath = NULL; + + if (hasPathSeparator(name)) + return loadIMEModulePath(name, loadedPath); + + handle = loadIMEModulePath(name, loadedPath); + if (handle) + return handle; + + if (!directory[0]) + return NULL; + + path = makeIMEModulePath(directory, "", name, ""); + if (path) + { + handle = loadIMEModulePath(path, loadedPath); + _glfw_free(path); + if (handle) + return handle; + } + + if (!hasModuleSuffix) + { + path = makeIMEModulePath(directory, "", name, + _GLFW_X11_IME_MODULE_SUFFIX); + if (path) + { + handle = loadIMEModulePath(path, loadedPath); + _glfw_free(path); + if (handle) + return handle; + } + } + + if (!hasModulePrefix) + { + path = makeIMEModulePath(directory, "glfw-", name, ""); + if (path) + { + handle = loadIMEModulePath(path, loadedPath); + _glfw_free(path); + if (handle) + return handle; + } + + if (!hasModuleSuffix) + { + path = makeIMEModulePath(directory, "glfw-", name, + _GLFW_X11_IME_MODULE_SUFFIX); + if (path) + { + handle = loadIMEModulePath(path, loadedPath); + _glfw_free(path); + if (handle) + return handle; + } + } + } + + return NULL; +} + +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 blockCount) +{ + int textBufferCount = preedit->textBufferCount; + int blockBufferCount = preedit->blockSizesBufferCount; + + 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; + } + + while (blockBufferCount < blockCount) + blockBufferCount = blockBufferCount ? blockBufferCount * 2 : 8; + + if (blockBufferCount != preedit->blockSizesBufferCount) + { + int* blocks = _glfw_realloc(preedit->blockSizes, + sizeof(int) * blockBufferCount); + if (!blocks) + return; + + preedit->blockSizes = blocks; + preedit->blockSizesBufferCount = blockBufferCount; + } +} + +static void hostUpdatePreedit(void* handle, + const char* utf8, + int caret, + const int* blockSizes, + int blockCount, + int focusedBlock) +{ + _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; + if (!blockSizes || blockCount <= 0) + blockCount = count ? 1 : 0; + + ensurePreeditBuffers(preedit, count, blockCount); + 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 = blockCount; + if (blockSizes && blockCount > 0) + { + for (int i = 0; i < blockCount; i++) + preedit->blockSizes[i] = blockSizes[i]; + } + else if (count) + preedit->blockSizes[0] = count; + + if (focusedBlock < 0 || focusedBlock >= blockCount) + focusedBlock = 0; + preedit->focusedBlockIndex = focusedBlock; + 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; + char* loadedPath = NULL; + + if (!path || !*path) + return GLFW_FALSE; + + _glfw.x11.imeModule.debug = debug && *debug && strcmp(debug, "0") != 0; + + _glfw.x11.imeModule.handle = loadIMEModuleName(path, &loadedPath); + 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); + _glfw_free(loadedPath); + 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", + loadedPath ? loadedPath : path); + } + + _glfw_free(loadedPath); + + 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..461b740a27 --- /dev/null +++ b/src/x11_ime_module.h @@ -0,0 +1,102 @@ +//======================================================================== +// 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 2 + +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, + const int* block_sizes, + int block_count, + int focused_block); + 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 737b911cbe..c9f66fd63d 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 293b91264d..415939c2f0 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 4f4b3946c2..73ab1c7ea7 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -244,6 +244,27 @@ 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; +} + +static KeySym getIMEKeySym(XKeyEvent* event, int keycode) +{ + KeySym keysym = NoSymbol; + XLookupString(event, NULL, 0, &keysym, NULL); + + if (keysym != NoSymbol) + return keysym; + + return getKeySym(event, keycode); +} + // Sends an EWMH or ICCCM event to the window manager // static void sendEventToWM(_GLFWwindow* window, Atom type, @@ -1357,12 +1378,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 +1478,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 imeKeysym = getIMEKeySym(&event->xkey, keycode); + + if (_glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) imeKeysym, + 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 +1556,9 @@ static void processEvent(XEvent *event) _glfwInputChar(window, codepoint, mods, plain); } + if (imeModuleActive) + _glfwNotifyNormalKeyIMEModuleX11(window); + return; } @@ -1526,6 +1566,7 @@ static void processEvent(XEvent *event) { const int key = translateKey(keycode); const int mods = translateState(event->xkey.state); + const KeySym imeKeysym = getIMEKeySym(&event->xkey, keycode); if (!_glfw.x11.xkb.detectable) { @@ -1559,6 +1600,14 @@ static void processEvent(XEvent *event) } } + if (_glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) imeKeysym, + 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 +2015,8 @@ static void processEvent(XEvent *event) { XSetICFocus(window->x11.ic); } + else if (!window->textInputFocusInitialized || window->textInputFocus) + _glfwFocusInIMEModuleX11(window); _glfwInputWindowFocus(window, GLFW_TRUE); return; @@ -1988,6 +2039,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 +2326,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 +2365,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 +3114,8 @@ GLFWbool _glfwRawMouseMotionSupportedX11(void) void _glfwPollEventsX11(void) { drainEmptyEvents(); + _glfwRefreshPendingCursorRectsIMEModuleX11("event-drain"); + _glfwDrainIMEModuleX11(); #if defined(GLFW_BUILD_LINUX_JOYSTICK) if (_glfw.joysticksInitialized) @@ -3068,6 +3128,8 @@ void _glfwPollEventsX11(void) XEvent event; XNextEvent(_glfw.x11.display, &event); processEvent(&event); + _glfwRefreshPendingCursorRectsIMEModuleX11("after-event"); + _glfwDrainIMEModuleX11(); } _GLFWwindow* window = _glfw.x11.disabledCursorWindow; @@ -3086,6 +3148,8 @@ void _glfwPollEventsX11(void) } XFlush(_glfw.x11.display); + _glfwRefreshPendingCursorRectsIMEModuleX11("poll-end"); + _glfwDrainIMEModuleX11(); } void _glfwWaitEventsX11(void) @@ -3366,6 +3430,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 +3460,12 @@ void _glfwResetPreeditTextX11(_GLFWwindow* window) XVaNestedList preedit_attr; char* result; + if (_glfwHasIMEModuleX11()) + { + _glfwResetIMEModuleX11(window); + return; + } + if (!ic) return; @@ -3419,6 +3499,12 @@ void _glfwSetIMEStatusX11(_GLFWwindow* window, int active) { XIC ic = window->x11.ic; + if (_glfwHasIMEModuleX11()) + { + _glfwSetStatusIMEModuleX11(window, active); + return; + } + if (!ic) return; @@ -3450,6 +3536,9 @@ void _glfwSetTextInputFocusX11(_GLFWwindow* window, GLFWbool focused) int _glfwGetIMEStatusX11(_GLFWwindow* window) { + if (_glfwHasIMEModuleX11()) + return _glfwGetStatusIMEModuleX11(window); + if (!window->x11.ic) return GLFW_FALSE; From 714d7bb1a417fc798366c9c773448e021ea0bb5d Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Sat, 20 Jun 2026 10:35:47 +0900 Subject: [PATCH 2/8] 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 73ab1c7ea7..f7bc1c403a 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -1385,7 +1385,11 @@ static void processEvent(XEvent *event) keycode = event->xkey.keycode; if (!imeModuleActive) + { filtered = XFilterEvent(event, None); + if (filtered) + return; + } if (_glfw.x11.randr.available) { @@ -1479,20 +1483,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 imeKeysym = getIMEKeySym(&event->xkey, keycode); + GLFWbool moduleHandled = GLFW_FALSE; - if (_glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) imeKeysym, - 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) imeKeysym, + 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 a6987b24719fde6c6df0ca415d4930d25101e1b0 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Wed, 24 Jun 2026 14:34:22 +0900 Subject: [PATCH 3/8] 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 | 25 +++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md index d678ff78ea..c2647ca53e 100644 --- a/docs/ime-ibus-prototype.md +++ b/docs/ime-ibus-prototype.md @@ -205,6 +205,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 f7bc1c403a..27d5fa9af7 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -1483,9 +1483,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 imeKeysym = getIMEKeySym(&event->xkey, keycode); + const GLFWbool textInputFocused = + !window->textInputFocusInitialized || window->textInputFocus; GLFWbool moduleHandled = GLFW_FALSE; - if (imeModuleActive) + if (imeModuleActive && textInputFocused) { moduleHandled = _glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) imeKeysym, @@ -1573,6 +1575,8 @@ static void processEvent(XEvent *event) const int key = translateKey(keycode); const int mods = translateState(event->xkey.state); const KeySym imeKeysym = getIMEKeySym(&event->xkey, keycode); + const GLFWbool textInputFocused = + !window->textInputFocusInitialized || window->textInputFocus; if (!_glfw.x11.xkb.detectable) { @@ -1606,7 +1610,8 @@ static void processEvent(XEvent *event) } } - if (_glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) imeKeysym, + if (imeModuleActive && textInputFocused && + _glfwProcessKeyIMEModuleX11(window, keycode, (unsigned int) imeKeysym, event->xkey.state, GLFW_RELEASE, mods, event->xkey.time)) { @@ -3528,6 +3533,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 8e809a3ba78ec1ef340f09dd59615f30b0c885d9 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Wed, 24 Jun 2026 15:57:17 +0900 Subject: [PATCH 4/8] 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 c2647ca53e..ae53ec0157 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 @@ -130,6 +132,8 @@ The module uses these IBus input context methods and signals: - `FocusOut` - `SetCursorLocation` - `Reset` +- `Enabled` +- `Disabled` The module currently uses `SetCursorLocation`, not `SetCursorLocationRelative`. @@ -188,6 +192,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 @@ -215,6 +224,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. @@ -261,6 +275,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 @@ -297,11 +312,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 @@ -317,10 +336,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 39040b4a9efd8949d7f23305477870aee904a43c Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Wed, 24 Jun 2026 16:52:35 +0900 Subject: [PATCH 5/8] X11: Add bundled IBus compatibility modes Add two switches for game/application compatibility testing. GLFW_INFER_TEXT_INPUT_FOCUS infers text input focus from legacy IME and cursor input state, and also acts as the runtime override environment variable. This lets applications that already toggle GLFW_IME or cursor mode use the more portable text-input-focus semantics without code changes. In this mode, cursor mode changes are treated as text input focus hints. GLFW keeps the application-requested text input focus state separate from the effective platform focus, so cursor-disabled gameplay can suppress text input while cursor-normal text entry can re-enable it. This also helps applications that switch between gameplay and text entry with cursor mode only. glfwGetInputMode(GLFW_IME) keeps reporting the native IME status until the inference path initializes text input focus state for the window. After that it returns the application-requested text input focus state, 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 | 5 +++ docs/ime-ibus-prototype.md | 30 ++++++++++++++++ src/CMakeLists.txt | 16 +++++++++ src/input.c | 63 ++++++++++++++++++++++++++++++-- src/internal.h | 3 ++ src/x11_ime_module.c | 73 ++++++++++++++++++++++++-------------- src/x11_ime_module.h | 18 +++++----- 7 files changed, 170 insertions(+), 38 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ced37f885a..89af4530d3 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_INFER_TEXT_INPUT_FOCUS + "Infer text input focus from legacy IME and cursor input state" OFF) include(GNUInstallDirs) include(CMakeDependentOption) @@ -37,6 +39,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_INSTALL_IME_MODULES "Install experimental X11 IME modules" OFF "UNIX;NOT APPLE;GLFW_BUILD_X11;GLFW_INSTALL" OFF) diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md index ae53ec0157..a2069ad6a9 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. + The standalone IBus IME module is not installed by default. Configure with `-DGLFW_INSTALL_IME_MODULES=ON` to install `glfw-ibus.so`. The default install location is `${CMAKE_INSTALL_LIBDIR}/glfw` relative to the install prefix, @@ -56,6 +61,8 @@ When a module is active, the X11 backend: - skips XIM setup for that run - forwards X11 key events to the module +- passes the X event keysym, including modifier-translated printable keysyms, + to IBus `ProcessKeyEvent` - forwards focus changes to the module - translates preedit cursor rectangles from client-area coordinates to X11 root coordinates @@ -84,6 +91,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 @@ -229,6 +241,24 @@ 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 can infer text input focus from +legacy IME and cursor input state. Set `GLFW_INFER_TEXT_INPUT_FOCUS` to a +non-zero value, or build GLFW with `GLFW_INFER_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. + +When this compatibility mode is active, GLFW also treats cursor mode changes as +text input focus hints. The requested text input focus state is kept separately +from the effective platform focus, so cursor-disabled gameplay can suppress IME +routing while cursor-normal text entry can re-enable it. This helps existing +applications that switch between gameplay and text entry with cursor mode only. + +`glfwGetInputMode(window, GLFW_IME)` still returns the native platform IME +status until the remapping path has initialized text input focus state for the +window. After that, it returns the application-requested text input focus +state, not the cursor-mode-masked effective platform focus. 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 ae08ada32e..9651b4b949 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -144,6 +144,10 @@ target_include_directories(glfw PRIVATE "${GLFW_BINARY_DIR}/src") target_link_libraries(glfw PRIVATE Threads::Threads) +if (GLFW_INFER_TEXT_INPUT_FOCUS) + target_compile_definitions(glfw PRIVATE _GLFW_INFER_TEXT_INPUT_FOCUS) +endif() + if (GLFW_BUILD_WIN32) list(APPEND glfw_PKG_LIBS "-lgdi32") endif() @@ -221,10 +225,22 @@ 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 (GLFW_INSTALL_IME_MODULES AND NOT DBUS1_FOUND) message(FATAL_ERROR "D-Bus development package is required for installing X11 IME modules") 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 96fa01c76c..5bfeffef21 100644 --- a/src/input.c +++ b/src/input.c @@ -593,6 +593,31 @@ void _glfwCenterCursorInContentArea(_GLFWwindow* window) _glfw.platform.setCursorPos(window, width / 2.0, height / 2.0); } +GLFWbool _glfwInferTextInputFocus(void) +{ + const char* value = getenv("GLFW_INFER_TEXT_INPUT_FOCUS"); + + if (value && *value) + return strcmp(value, "0") != 0; + +#if defined(_GLFW_INFER_TEXT_INPUT_FOCUS) + return GLFW_TRUE; +#else + return GLFW_FALSE; +#endif +} + +static GLFWbool getEffectiveTextInputFocus(_GLFWwindow* window) +{ + if (_glfwInferTextInputFocus()) + { + return window->textInputFocusRequested && + window->cursorMode == GLFW_CURSOR_NORMAL; + } + + return window->textInputFocusRequested; +} + ////////////////////////////////////////////////////////////////////////// ////// GLFW public API ////// @@ -620,6 +645,12 @@ GLFWAPI int glfwGetInputMode(GLFWwindow* handle, int mode) case GLFW_UNLIMITED_MOUSE_BUTTONS: return window->disableMouseButtonLimit; case GLFW_IME: + if (_glfwInferTextInputFocus() && + window->textInputFocusInitialized) + { + return window->textInputFocusRequested; + } + return _glfw.platform.getIMEStatus(window); } @@ -658,6 +689,23 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) &window->virtualCursorPosX, &window->virtualCursorPosY); _glfw.platform.setCursorMode(window, value); + if (_glfwInferTextInputFocus()) + { + if (!window->textInputFocusInitialized) + { + window->textInputFocusInitialized = GLFW_TRUE; + window->textInputFocusRequested = GLFW_TRUE; + window->textInputFocus = GLFW_TRUE; + } + + const GLFWbool focused = getEffectiveTextInputFocus(window); + + if (window->textInputFocus != focused) + { + window->textInputFocus = focused; + _glfw.platform.setTextInputFocus(window, focused); + } + } return; } @@ -737,6 +785,16 @@ GLFWAPI void glfwSetInputMode(GLFWwindow* handle, int mode, int value) case GLFW_IME: { + if (_glfwInferTextInputFocus()) + { + value = value ? GLFW_TRUE : GLFW_FALSE; + window->textInputFocusInitialized = GLFW_TRUE; + window->textInputFocusRequested = value; + window->textInputFocus = getEffectiveTextInputFocus(window); + _glfw.platform.setTextInputFocus(window, window->textInputFocus); + return; + } + _glfw.platform.setIMEStatus(window, value ? GLFW_TRUE : GLFW_FALSE); return; } @@ -1049,8 +1107,9 @@ GLFWAPI void glfwSetTextInputFocus(GLFWwindow* handle, int focused) focused = focused ? GLFW_TRUE : GLFW_FALSE; window->textInputFocusInitialized = GLFW_TRUE; - window->textInputFocus = focused; - _glfw.platform.setTextInputFocus(window, focused); + window->textInputFocusRequested = focused; + window->textInputFocus = getEffectiveTextInputFocus(window); + _glfw.platform.setTextInputFocus(window, window->textInputFocus); } GLFWAPI unsigned int* glfwGetPreeditCandidate(GLFWwindow* handle, int index, int* textCount) diff --git a/src/internal.h b/src/internal.h index 57b43315d7..39a77aee7d 100644 --- a/src/internal.h +++ b/src/internal.h @@ -596,6 +596,7 @@ struct _GLFWwindow // Preserve legacy text input behavior for backward compatibility until // glfwSetTextInputFocus is used for this window. GLFWbool textInputFocusInitialized; + GLFWbool textInputFocusRequested; GLFWbool textInputFocus; int cursorMode; @@ -1018,6 +1019,8 @@ void _glfwInputError(int code, const char* format, ...); GLFWbool _glfwSelectPlatform(int platformID, _GLFWplatform* platform); +GLFWbool _glfwInferTextInputFocus(void); + GLFWbool _glfwStringInExtensionString(const char* string, const char* extensions); const _GLFWfbconfig* _glfwChooseFBConfig(const _GLFWfbconfig* desired, const _GLFWfbconfig* alternatives, diff --git a/src/x11_ime_module.c b/src/x11_ime_module.c index 8e65710f7a..2cccb95643 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 + #if !defined(_GLFW_X11_IME_MODULE_DIR) #define _GLFW_X11_IME_MODULE_DIR "" #endif @@ -289,39 +293,52 @@ GLFWbool _glfwLoadIMEModuleX11(void) PFN_glfwGetX11IMEBackend getBackend; char* loadedPath = NULL; - 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 = loadIMEModuleName(path, &loadedPath); - 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 = loadIMEModuleName(path, &loadedPath); + 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); - _glfw_free(loadedPath); - 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); + _glfw_free(loadedPath); + 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", - loadedPath ? loadedPath : path); - } + if (_glfw.x11.imeModule.debug) + { + fprintf(stderr, "GLFW IME: loaded external module %s\n", + loadedPath ? loadedPath : path); + } - _glfw_free(loadedPath); + _glfw_free(loadedPath); + } +#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; @@ -339,7 +356,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; } @@ -350,7 +368,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); From 2f49ac4d124cf767afe74b2e39ec28dcecac12b3 Mon Sep 17 00:00:00 2001 From: Daijiro Fukuda Date: Fri, 8 Jul 2022 09:45:00 +0900 Subject: [PATCH 6/8] Support soft fullscreen mode for each platform Add a new window hint: `GLFW_SOFT_FULLSCREEN`. This is especially necessary for the IME(Input Method Editor/Engine) to display properly on the fullscreen window. **Win32** By default, the fullscreen window is a strong exclusive exceeding HWND_TOPMOST and it doen't display OS notifications such as the sound volumn changed. This hint enables to display other views poping up on the fullscreen window. This can also effect transparency to fullscreen. **macOS** By default, the fullscreen window is exclusive. This hint uses macOS's default fullscreen (the behavior of the green button on the window's header). We don't need `monitor` in this mode, so it is not used. Specifying `monitor` in `glfwCreateWindow` makes window fullscreened, but the value is not used inside. The args of `glfwSetWindowMonitor` are not used except for `window` and the function just toggles the fullscreened state. These specifications have been made so that we can use this mode as similar to the normal default mode as possible. **X11** This hint manages XProperty: `NET_WM_BYPASS_COMPOSITOR`. By default, the compositing of the window is disabled to improve rendering performance. However, this can cause problems with other views poping up on the window, such as IME. This hint enables the compositing and the views poping up on the fullscreen to be displayed correctly. --- docs/window.md | 7 +++++++ include/GLFW/glfw3.h | 6 ++++++ src/cocoa_window.m | 47 ++++++++++++++++++++++++++++++++++++++++++++ src/internal.h | 1 + src/win32_window.c | 8 ++++++-- src/window.c | 4 ++++ src/x11_window.c | 3 ++- 7 files changed, 73 insertions(+), 3 deletions(-) diff --git a/docs/window.md b/docs/window.md index 2140f0975b..6f80e6d3d9 100644 --- a/docs/window.md +++ b/docs/window.md @@ -277,6 +277,12 @@ either or both of these hints are set to `GLFW_ANY_POSITION` then the window manager will position the window where it thinks the user will prefer it. Possible values are any valid screen coordinates and `GLFW_ANY_POSITION`. +@anchor GLFW_SOFT_FULLSCREEN_hint +__GLFW_SOFT_FULLSCREEN__ specifies whether full screen windows should avoid +exclusive full screen behavior where supported. This can allow system UI such +as input method windows, notifications or overlays to appear above a full screen +window. Possible values are `GLFW_TRUE` and `GLFW_FALSE`. + #### Framebuffer related hints {#window_hints_fb} @@ -548,6 +554,7 @@ GLFW_SCALE_FRAMEBUFFER | `GLFW_TRUE` | `GLFW_TRUE` or `GL GLFW_MOUSE_PASSTHROUGH | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` GLFW_POSITION_X | `GLFW_ANY_POSITION` | Any valid screen x-coordinate or `GLFW_ANY_POSITION` GLFW_POSITION_Y | `GLFW_ANY_POSITION` | Any valid screen y-coordinate or `GLFW_ANY_POSITION` +GLFW_SOFT_FULLSCREEN | `GLFW_FALSE` | `GLFW_TRUE` or `GLFW_FALSE` GLFW_RED_BITS | 8 | 0 to `INT_MAX` or `GLFW_DONT_CARE` GLFW_GREEN_BITS | 8 | 0 to `INT_MAX` or `GLFW_DONT_CARE` GLFW_BLUE_BITS | 8 | 0 to `INT_MAX` or `GLFW_DONT_CARE` diff --git a/include/GLFW/glfw3.h b/include/GLFW/glfw3.h index 06ba3dde4b..d62479d75b 100644 --- a/include/GLFW/glfw3.h +++ b/include/GLFW/glfw3.h @@ -941,6 +941,12 @@ extern "C" { */ #define GLFW_POSITION_Y 0x0002000F +/*! @brief Soft full screen window hint. + * + * Soft full screen [window hint](@ref GLFW_SOFT_FULLSCREEN_hint). + */ +#define GLFW_SOFT_FULLSCREEN 0x00020010 + /*! @brief Framebuffer bit depth hint. * * Framebuffer bit depth [hint](@ref GLFW_RED_BITS). diff --git a/src/cocoa_window.m b/src/cocoa_window.m index 81b3549530..45b5e8aa4f 100644 --- a/src/cocoa_window.m +++ b/src/cocoa_window.m @@ -38,6 +38,35 @@ // having been (according to documentation) added in Mac OS X 10.7 #define NSWindowCollectionBehaviorFullScreenNone (1 << 9) +// Returns whether the window is in macOS native full screen +// +static GLFWbool isSoftFullscreen(_GLFWwindow* window) +{ + return [window->ns.object styleMask] & NSWindowStyleMaskFullScreen; +} + +// Set macOS native full screen without switching monitor video modes +// +static void setSoftFullscreen(_GLFWwindow* window, GLFWbool enabled) +{ + if (isSoftFullscreen(window) == enabled) + return; + + NSUInteger styleMask = [window->ns.object styleMask]; + + // Native full screen requires a resizable window style + styleMask |= NSWindowStyleMaskResizable; + [window->ns.object setStyleMask:styleMask]; + + [window->ns.object toggleFullScreen:nil]; + + if (!window->resizable) + { + styleMask &= ~NSWindowStyleMaskResizable; + [window->ns.object setStyleMask:styleMask]; + } +} + // Returns whether the cursor is in the content area of the specified window // static GLFWbool cursorInContentArea(_GLFWwindow* window) @@ -1062,6 +1091,13 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window, { @autoreleasepool { + GLFWbool softFullscreen = GLFW_FALSE; + if (_glfw.hints.window.softFullscreen && window->monitor) + { + softFullscreen = GLFW_TRUE; + _glfwInputWindowMonitor(window, NULL); + } + if (!createNativeWindow(window, wndconfig, fbconfig)) return GLFW_FALSE; @@ -1126,6 +1162,9 @@ GLFWbool _glfwCreateWindowCocoa(_GLFWwindow* window, name:NSTextInputContextKeyboardSelectionDidChangeNotification object:nil]; + if (softFullscreen) + setSoftFullscreen(window, GLFW_TRUE); + return GLFW_TRUE; } // autoreleasepool @@ -1394,6 +1433,14 @@ void _glfwSetWindowMonitorCocoa(_GLFWwindow* window, { @autoreleasepool { + if (_glfw.hints.window.softFullscreen) + { + setSoftFullscreen(window, monitor != NULL); + + if (monitor) + return; + } + if (window->monitor == monitor) { if (monitor) diff --git a/src/internal.h b/src/internal.h index 39a77aee7d..b2474fcc9c 100644 --- a/src/internal.h +++ b/src/internal.h @@ -419,6 +419,7 @@ struct _GLFWwndconfig bool mousePassthrough; bool scaleToMonitor; bool scaleFramebuffer; + bool softFullscreen; struct { char frameName[256]; } ns; diff --git a/src/win32_window.c b/src/win32_window.c index c9ea008781..3a57bfc9a1 100644 --- a/src/win32_window.c +++ b/src/win32_window.c @@ -467,7 +467,9 @@ static void fitToMonitor(_GLFWwindow* window) mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left, - mi.rcMonitor.bottom - mi.rcMonitor.top, + _glfw.hints.window.softFullscreen ? + mi.rcMonitor.bottom - mi.rcMonitor.top + 1 : + mi.rcMonitor.bottom - mi.rcMonitor.top, SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOCOPYBITS); } @@ -2309,7 +2311,9 @@ void _glfwSetWindowMonitorWin32(_GLFWwindow* window, mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right - mi.rcMonitor.left, - mi.rcMonitor.bottom - mi.rcMonitor.top, + _glfw.hints.window.softFullscreen ? + mi.rcMonitor.bottom - mi.rcMonitor.top + 1 : + mi.rcMonitor.bottom - mi.rcMonitor.top, flags); } else diff --git a/src/window.c b/src/window.c index 68e0c597f9..495198a67b 100644 --- a/src/window.c +++ b/src/window.c @@ -280,6 +280,7 @@ void glfwDefaultWindowHints(void) _glfw.hints.window.xpos = GLFW_ANY_POSITION; _glfw.hints.window.ypos = GLFW_ANY_POSITION; _glfw.hints.window.scaleFramebuffer = true; + _glfw.hints.window.softFullscreen = false; // The default is 24 bits of color, 24 bits of depth and 8 bits of stencil, // double buffered @@ -402,6 +403,9 @@ GLFWAPI void glfwWindowHint(int hint, int value) case GLFW_MOUSE_PASSTHROUGH: _glfw.hints.window.mousePassthrough = value; return; + case GLFW_SOFT_FULLSCREEN: + _glfw.hints.window.softFullscreen = value; + return; case GLFW_CLIENT_API: _glfw.hints.context.client = value; return; diff --git a/src/x11_window.c b/src/x11_window.c index 27d5fa9af7..be21dbc387 100644 --- a/src/x11_window.c +++ b/src/x11_window.c @@ -386,7 +386,8 @@ static void updateWindowMode(_GLFWwindow* window) // Enable compositor bypass if (!window->x11.transparent) { - const unsigned long value = 1; + const unsigned long value = + _glfw.hints.window.softFullscreen ? 2 : 1; XChangeProperty(_glfw.x11.display, window->x11.handle, _glfw.x11.NET_WM_BYPASS_COMPOSITOR, XA_CARDINAL, 32, From d91684648c730a5e5f6a18ae56e6a4d665d5bb59 Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Fri, 17 Jul 2026 08:38:57 +0900 Subject: [PATCH 7/8] X11: Improve IBus address and keycode handling Search the IBus bus directory for a matching machine-id prefixed address file when the DISPLAY-derived name is not present. This handles sessions where IBus writes a Wayland-named bus file even though the X11 backend is running through Xwayland. Also pass IBus the hardware-style keycode expected by ProcessKeyEvent instead of the raw X11 keycode. Keep both values in the debug log so backend behavior remains observable. --- src/x11_ibus_module.c | 120 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 99 insertions(+), 21 deletions(-) diff --git a/src/x11_ibus_module.c b/src/x11_ibus_module.c index de4a1c663f..8591e359c3 100644 --- a/src/x11_ibus_module.c +++ b/src/x11_ibus_module.c @@ -7,6 +7,7 @@ #include "x11_ime_module.h" #include +#include #include #include #include @@ -784,16 +785,83 @@ static DBusHandlerResult dbus_filter(DBusConnection* connection, return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; } +static int read_ibus_address_file(const char* path, char* buffer, size_t size) +{ + FILE* 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 read_ibus_address_from_bus_dir(const char* directory, + const char* machine_id, + char* buffer, + size_t size) +{ + DIR* dir; + struct dirent* entry; + const size_t machine_id_length = strlen(machine_id); + + dir = opendir(directory); + if (!dir) + return GLFW_FALSE; + + while ((entry = readdir(dir))) + { + char* path; + size_t path_size; + + if (strncmp(entry->d_name, machine_id, machine_id_length) != 0 || + entry->d_name[machine_id_length] != '-') + { + continue; + } + + path_size = strlen(directory) + 1 + strlen(entry->d_name) + 1; + path = malloc(path_size); + if (!path) + continue; + + snprintf(path, path_size, "%s/%s", directory, entry->d_name); + if (read_ibus_address_file(path, buffer, size)) + { + free(path); + closedir(dir); + return GLFW_TRUE; + } + + free(path); + } + + closedir(dir); + return GLFW_FALSE; +} + static int read_ibus_address(char* buffer, size_t size) { const char* address = getenv("IBUS_ADDRESS"); - char path[4096]; + char bus_dir[4096]; char display[128]; const char* config; const char* home; char* machine_id; DBusError error; - FILE* file; if (address && *address) { @@ -826,36 +894,45 @@ static int read_ibus_address(char* buffer, size_t size) } if (config && *config) - snprintf(path, sizeof(path), "%s/ibus/bus/%s-%s-%s", config, machine_id, host, number); + snprintf(bus_dir, sizeof(bus_dir), "%s/ibus/bus", config); else if (home && *home) - snprintf(path, sizeof(path), "%s/.config/ibus/bus/%s-%s-%s", home, machine_id, host, number); + snprintf(bus_dir, sizeof(bus_dir), "%s/.config/ibus/bus", home); else { dbus_free(machine_id); return GLFW_FALSE; } - dbus_free(machine_id); + { + char* path; + size_t path_size = strlen(bus_dir) + 1 + strlen(machine_id) + 1 + + strlen(host) + 1 + strlen(number) + 1; - file = fopen(path, "r"); - if (!file) - return GLFW_FALSE; + path = malloc(path_size); + if (!path) + { + dbus_free(machine_id); + return GLFW_FALSE; + } - while (fgets(buffer, size, file)) - { - if (strncmp(buffer, "IBUS_ADDRESS=", 13) == 0) + snprintf(path, path_size, "%s/%s-%s-%s", bus_dir, machine_id, host, number); + if (read_ibus_address_file(path, buffer, size)) { - char* value = buffer + 13; - char* newline = strchr(value, '\n'); - if (newline) - *newline = '\0'; - memmove(buffer, value, strlen(value) + 1); - fclose(file); + free(path); + dbus_free(machine_id); return GLFW_TRUE; } + + free(path); } - fclose(file); + if (read_ibus_address_from_bus_dir(bus_dir, machine_id, buffer, size)) + { + dbus_free(machine_id); + return GLFW_TRUE; + } + + dbus_free(machine_id); return GLFW_FALSE; } @@ -946,7 +1023,8 @@ static void process_key_command(GLFWx11IMEBackend* backend, Command* command) DBusMessage* reply; dbus_bool_t handled = FALSE; dbus_uint32_t keyval = request->event.keysym; - dbus_uint32_t keycode = request->event.keycode; + dbus_uint32_t x11_keycode = request->event.keycode; + dbus_uint32_t keycode = x11_keycode >= 8 ? x11_keycode - 8 : x11_keycode; dbus_uint32_t state = ibus_state_from_glfw(request->event.mods, request->event.action); @@ -957,9 +1035,9 @@ static void process_key_command(GLFWx11IMEBackend* backend, Command* command) 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 start id=%lu key_serial=%lu timestamp=%.6f keyval=0x%x x11_keycode=%u ibus_keycode=%u state=0x%x", request->id, request->event.time, request->queued_at, - keyval, keycode, state); + keyval, x11_keycode, keycode, state); if (request->event.cursor_rect_valid) { From 8cd2c3e464289e1eae34f00d63649fcfd1727c3c Mon Sep 17 00:00:00 2001 From: Takuro Ashie Date: Fri, 17 Jul 2026 22:47:55 +0900 Subject: [PATCH 8/8] X11: Add native Fcitx5 IME module prototype --- docs/ime-ibus-prototype.md | 67 +- src/CMakeLists.txt | 21 +- src/x11_fcitx5_module.c | 1404 ++++++++++++++++++++++++++++++++++++ 3 files changed, 1483 insertions(+), 9 deletions(-) create mode 100644 src/x11_fcitx5_module.c diff --git a/docs/ime-ibus-prototype.md b/docs/ime-ibus-prototype.md index a2069ad6a9..1e08687a17 100644 --- a/docs/ime-ibus-prototype.md +++ b/docs/ime-ibus-prototype.md @@ -41,16 +41,18 @@ 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. -The standalone IBus IME module is not installed by default. Configure with -`-DGLFW_INSTALL_IME_MODULES=ON` to install `glfw-ibus.so`. The default install -location is `${CMAKE_INSTALL_LIBDIR}/glfw` relative to the install prefix, -typically `lib/glfw` under that prefix. For example, with the default -`/usr/local` prefix this is usually `/usr/local/lib/glfw`. +The standalone IME modules are not installed by default. Configure with +`-DGLFW_INSTALL_IME_MODULES=ON` to install `glfw-ibus.so` and +`glfw-fcitx5.so`. The default install location is +`${CMAKE_INSTALL_LIBDIR}/glfw` relative to the install prefix, typically +`lib/glfw` under that prefix. For example, with the default `/usr/local` +prefix this is usually `/usr/local/lib/glfw`. When `GLFW_IM_MODULE` does not contain a `/`, GLFW also searches this default module directory. The `glfw-` prefix and platform module suffix may be omitted, -so `GLFW_IM_MODULE=ibus` loads the installed `glfw-ibus.so` module. Full paths -continue to be loaded as specified. +so `GLFW_IM_MODULE=ibus` and `GLFW_IM_MODULE=fcitx5` load the installed +`glfw-ibus.so` and `glfw-fcitx5.so` modules. Full paths continue to be loaded +as specified. ### GLFW Core @@ -89,7 +91,8 @@ 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. +package is available. A separate native Fcitx5 experiment is built as +`glfw-fcitx5.so` from the same private X11 IME module ABI. 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 @@ -107,6 +110,54 @@ The module owns: - request and event queues - timing instrumentation +### Native Fcitx5 Experiment + +The native Fcitx5 module is a sibling of the IBus module. It is loaded with the +same runtime mechanism: + +```sh +GLFW_IM_MODULE=/path/to/glfw-fcitx5.so ./application +``` + +It keeps Fcitx5 and D-Bus protocol knowledge inside the module. GLFW core still +only sees the private X11 IME backend ABI. + +The module connects to Fcitx5 through the session bus name `org.fcitx.Fcitx5`, +creates an input context with `org.fcitx.Fcitx.InputMethod1.CreateInputContext`, +then talks to the returned `org.fcitx.Fcitx.InputContext1` object. It currently +uses: + +- `SetCapability` +- `FocusIn` +- `FocusOut` +- `Reset` +- `SetCursorRect` +- `ProcessKeyEvent` +- `CommitString` +- `UpdateFormattedPreedit` +- `NotifyFocusOut` + +The first version intentionally maps Fcitx5 formatted preedit segments directly +to GLFW preedit blocks. This preserves useful segmentation without exposing +Fcitx5 formatting values to GLFW core. + +The module treats a non-zero formatted-preedit segment format as the focused +GLFW preedit block. If no formatted segment is marked, it falls back to using +the reported caret position. + +Unlike the IBus module, the Fcitx5 module sends the X11 keycode received from +GLFW directly to `ProcessKeyEvent`. Fcitx5 appears to interpret this field as +an XKB keycode. Using the IBus-style `keycode - 8` conversion caused obvious +layout mismatches, such as the Enter key being interpreted as `t`. + +The module retries connection to Fcitx5 when startup, input context creation or +key processing fails. While disconnected, focus state is retained, key events +are reported as not handled, and the worker retries connection periodically. + +Native Fcitx5 status control is intentionally left as future work. The current +prototype keeps status behavior minimal and focuses on commit, preedit, +candidate positioning and key processing. + ### Worker Thread The module creates a worker thread. The worker thread owns all D-Bus traffic. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9651b4b949..b57794d81e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -259,8 +259,27 @@ if (GLFW_BUILD_X11) install(TARGETS glfw-ibus LIBRARY DESTINATION "${GLFW_IME_MODULE_INSTALL_DIR}") endif() + + add_library(glfw-fcitx5 MODULE x11_fcitx5_module.c x11_ime_module.h) + set_target_properties(glfw-fcitx5 PROPERTIES + PREFIX "" + C_STANDARD 99 + C_EXTENSIONS OFF + FOLDER "GLFW3") + target_include_directories(glfw-fcitx5 PRIVATE + "${GLFW_SOURCE_DIR}/src" + "${GLFW_SOURCE_DIR}/include" + ${DBUS1_INCLUDE_DIRS}) + target_link_libraries(glfw-fcitx5 PRIVATE Threads::Threads ${DBUS1_LIBRARIES}) + target_compile_options(glfw-fcitx5 PRIVATE ${DBUS1_CFLAGS_OTHER}) + target_link_directories(glfw-fcitx5 PRIVATE ${DBUS1_LIBRARY_DIRS}) + + if (GLFW_INSTALL_IME_MODULES) + install(TARGETS glfw-fcitx5 + LIBRARY DESTINATION "${GLFW_IME_MODULE_INSTALL_DIR}") + endif() else() - message(STATUS "D-Bus development package not found; skipping glfw-ibus prototype module") + message(STATUS "D-Bus development package not found; skipping X11 IME prototype modules") endif() endif() endif() diff --git a/src/x11_fcitx5_module.c b/src/x11_fcitx5_module.c new file mode 100644 index 0000000000..85e110ece7 --- /dev/null +++ b/src/x11_fcitx5_module.c @@ -0,0 +1,1404 @@ +#define _POSIX_C_SOURCE 200809L + +//======================================================================== +// GLFW X11 Fcitx5 IME module prototype +//======================================================================== + +#include "x11_ime_module.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +static const char* FCITX5_SERVICE = "org.fcitx.Fcitx5"; +static const char* FCITX5_PATH = "/org/freedesktop/portal/inputmethod"; +static const char* FCITX5_INTERFACE = "org.fcitx.Fcitx.InputMethod1"; +static const char* FCITX5_INPUT_INTERFACE = "org.fcitx.Fcitx.InputContext1"; + +#define FCITX5_CAP_PREEDIT (1ULL << 1) +#define FCITX5_CAP_FORMATTED_PREEDIT (1ULL << 4) +#define FCITX5_CAP_CLIENT_UNFOCUS_COMMIT (1ULL << 5) +#define FCITX5_CAP_KEY_EVENT_ORDER_FIX (1ULL << 37) + +enum +{ + FCITX5_SHIFT_MASK = 1 << 0, + FCITX5_LOCK_MASK = 1 << 1, + FCITX5_CONTROL_MASK = 1 << 2, + FCITX5_MOD1_MASK = 1 << 3, + FCITX5_MOD2_MASK = 1 << 4, + FCITX5_MOD4_MASK = 1 << 6 +}; + +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; + 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 PreeditInfo +{ + char* text; + int caret; + int* block_sizes; + int block_count; + int focused_block; +} PreeditInfo; + +struct GLFWx11IMEBackend +{ + GLFWx11IMEHostAPI host; + pthread_t thread; + pthread_mutex_t mutex; + pthread_cond_t cond; + int running; + int ready; + int status; + double timeout_ms; + double reconnect_at; + double reconnect_interval; + 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; + int filter_added; +}; + +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-fcitx5: %s\n", buffer); +} + +static uint32_t fcitx5_state_from_glfw(unsigned int mods) +{ + uint32_t state = 0; + + if (mods & GLFW_MOD_SHIFT) + state |= FCITX5_SHIFT_MASK; + if (mods & GLFW_MOD_CAPS_LOCK) + state |= FCITX5_LOCK_MASK; + if (mods & GLFW_MOD_CONTROL) + state |= FCITX5_CONTROL_MASK; + if (mods & GLFW_MOD_ALT) + state |= FCITX5_MOD1_MASK; + if (mods & GLFW_MOD_NUM_LOCK) + state |= FCITX5_MOD2_MASK; + if (mods & GLFW_MOD_SUPER) + state |= FCITX5_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, + const int* block_sizes, + int block_count, + int focused_block) +{ + QueuedEvent* event = calloc(1, sizeof(QueuedEvent)); + unsigned long request_id; + Request* request; + + if (!event) + return; + + pthread_mutex_lock(&backend->mutex); + + // Fcitx5 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; + 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; + 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 caret=%i blocks=%i focused=%i text='%s'", + type, event->request_id, event->request_timed_out, + event->timestamp, event->caret, event->block_count, + event->focused_block, 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(FCITX5_SERVICE, + backend->input_context_path, + FCITX5_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 int utf8_count(const char* text) +{ + 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_fcitx5_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 int append_text(char** buffer, size_t* length, size_t* capacity, const char* text) +{ + size_t text_length; + char* next; + + if (!text) + text = ""; + + text_length = strlen(text); + if (*length + text_length + 1 > *capacity) + { + size_t next_capacity = *capacity ? *capacity * 2 : 32; + while (*length + text_length + 1 > next_capacity) + next_capacity *= 2; + + next = realloc(*buffer, next_capacity); + if (!next) + return GLFW_FALSE; + + *buffer = next; + *capacity = next_capacity; + } + + memcpy(*buffer + *length, text, text_length); + *length += text_length; + (*buffer)[*length] = '\0'; + return GLFW_TRUE; +} + +static int append_preedit_block_size(PreeditInfo* info, int size) +{ + int* block_sizes; + + if (size <= 0) + return GLFW_TRUE; + + block_sizes = realloc(info->block_sizes, + sizeof(int) * (size_t) (info->block_count + 1)); + if (!block_sizes) + return GLFW_FALSE; + + info->block_sizes = block_sizes; + info->block_sizes[info->block_count++] = size; + return GLFW_TRUE; +} + +static int parse_fcitx5_formatted_preedit(GLFWx11IMEBackend* backend, + DBusMessage* message, + PreeditInfo* info) +{ + DBusMessageIter iter, array; + size_t length = 0; + size_t capacity = 0; + int char_count = 0; + int segment_index = 0; + int focused_segment = -1; + + memset(info, 0, sizeof(*info)); + info->caret = -1; + + if (!dbus_message_iter_init(message, &iter) || + dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_ARRAY) + { + return GLFW_FALSE; + } + + dbus_message_iter_recurse(&iter, &array); + while (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_INVALID) + { + DBusMessageIter structure; + const char* segment = NULL; + dbus_int32_t format = 0; + int segment_count; + + if (dbus_message_iter_get_arg_type(&array) != DBUS_TYPE_STRUCT) + { + free(info->text); + free(info->block_sizes); + info->text = NULL; + info->block_sizes = NULL; + return GLFW_FALSE; + } + + dbus_message_iter_recurse(&array, &structure); + if (dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_STRING) + { + free(info->text); + free(info->block_sizes); + info->text = NULL; + info->block_sizes = NULL; + return GLFW_FALSE; + } + + dbus_message_iter_get_basic(&structure, &segment); + + if (!dbus_message_iter_next(&structure) || + dbus_message_iter_get_arg_type(&structure) != DBUS_TYPE_INT32) + { + free(info->text); + free(info->block_sizes); + info->text = NULL; + info->block_sizes = NULL; + return GLFW_FALSE; + } + + dbus_message_iter_get_basic(&structure, &format); + segment_count = utf8_count(segment); + + log_line(backend, + "preedit segment index=%i chars=%i format=%i text='%s'", + segment_index, segment_count, format, segment ? segment : ""); + + if (format != 0 && focused_segment < 0) + focused_segment = segment_index; + + if (!append_text(&info->text, &length, &capacity, segment) || + !append_preedit_block_size(info, segment_count)) + { + free(info->text); + free(info->block_sizes); + info->text = NULL; + info->block_sizes = NULL; + return GLFW_FALSE; + } + + char_count += segment_count; + segment_index++; + dbus_message_iter_next(&array); + } + + if (!info->text) + info->text = xstrdup(""); + + if (dbus_message_iter_next(&iter) && + dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_INT32) + { + dbus_int32_t caret = 0; + dbus_message_iter_get_basic(&iter, &caret); + info->caret = normalize_fcitx5_index(info->text, char_count, caret); + } + + if (info->caret < 0) + info->caret = char_count; + + if (info->block_count <= 0 && char_count > 0) + append_preedit_block_size(info, char_count); + + if (focused_segment >= 0 && focused_segment < info->block_count) + info->focused_block = focused_segment; + else + { + for (int i = 0, offset = 0; i < info->block_count; i++) + { + const int next = offset + info->block_sizes[i]; + if (info->caret >= offset && (info->caret < next || i == info->block_count - 1)) + { + info->focused_block = i; + break; + } + offset = next; + } + } + + return GLFW_TRUE; +} + +static int parse_create_input_context_reply(DBusMessage* reply, + const char** path) +{ + DBusMessageIter iter; + + *path = NULL; + + if (!dbus_message_iter_init(reply, &iter) || + dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_OBJECT_PATH) + { + return GLFW_FALSE; + } + + dbus_message_iter_get_basic(&iter, path); + return *path && **path; +} + +static DBusHandlerResult dbus_filter(DBusConnection* connection, + DBusMessage* message, + void* data) +{ + GLFWx11IMEBackend* backend = data; + PreeditInfo info; + (void) connection; + + if (dbus_message_is_signal(message, FCITX5_INPUT_INTERFACE, "CommitString")) + { + const char* text = NULL; + DBusError error; + + dbus_error_init(&error); + if (dbus_message_get_args(message, &error, + DBUS_TYPE_STRING, &text, + DBUS_TYPE_INVALID)) + { + enqueue_event(backend, EVENT_COMMIT, NULL, text ? text : "", -1, + NULL, 0, 0); + } + dbus_error_free(&error); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, FCITX5_INPUT_INTERFACE, "UpdateFormattedPreedit")) + { + if (parse_fcitx5_formatted_preedit(backend, message, &info)) + { + if (info.text && *info.text) + { + 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.text); + free(info.block_sizes); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(message, FCITX5_INPUT_INTERFACE, "NotifyFocusOut")) + { + enqueue_event(backend, EVENT_CLEAR_PREEDIT, NULL, NULL, -1, NULL, 0, 0); + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +static void disconnect_fcitx5(GLFWx11IMEBackend* backend, const char* reason) +{ + if (backend->connection || backend->input_context_path || backend->ready) + log_line(backend, "disconnecting from Fcitx5 reason=%s", reason ? reason : ""); + + backend->ready = GLFW_FALSE; + backend->active_request_id = 0; + backend->active_window = NULL; + + if (backend->connection) + { + if (backend->filter_added) + dbus_connection_remove_filter(backend->connection, dbus_filter, backend); + dbus_connection_close(backend->connection); + dbus_connection_unref(backend->connection); + backend->connection = NULL; + } + + free(backend->input_context_path); + backend->input_context_path = NULL; + backend->filter_added = GLFW_FALSE; + backend->reconnect_at = now_seconds(backend) + backend->reconnect_interval; +} + +static void complete_failed_request(GLFWx11IMEBackend* backend, Request* request) +{ + if (!request) + return; + + pthread_mutex_lock(&backend->mutex); + request->completed = GLFW_TRUE; + request->failed = GLFW_TRUE; + request->handled = GLFW_FALSE; + request->completed_at = now_seconds(backend); + pthread_cond_broadcast(&backend->cond); + pthread_mutex_unlock(&backend->mutex); + + release_request(backend, request); +} + +static int connect_fcitx5(GLFWx11IMEBackend* backend) +{ + DBusError error; + DBusMessage* message; + DBusMessage* reply; + const char* path = NULL; + dbus_uint64_t caps = FCITX5_CAP_PREEDIT | + FCITX5_CAP_FORMATTED_PREEDIT | + FCITX5_CAP_CLIENT_UNFOCUS_COMMIT | + FCITX5_CAP_KEY_EVENT_ORDER_FIX; + + log_line(backend, "connecting to Fcitx5 on session bus"); + + dbus_error_init(&error); + backend->connection = dbus_bus_get_private(DBUS_BUS_SESSION, &error); + if (!backend->connection) + { + log_line(backend, "failed to open session bus: %s", error.message ? error.message : ""); + dbus_error_free(&error); + backend->reconnect_at = now_seconds(backend) + backend->reconnect_interval; + return GLFW_FALSE; + } + + dbus_connection_set_exit_on_disconnect(backend->connection, FALSE); + + log_line(backend, "creating Fcitx5 input context"); + + message = dbus_message_new_method_call(FCITX5_SERVICE, FCITX5_PATH, + FCITX5_INTERFACE, "CreateInputContext"); + if (!message) + { + disconnect_fcitx5(backend, "CreateInputContext message allocation failed"); + return GLFW_FALSE; + } + + { + DBusMessageIter iter, array, structure; + const char* program_key = "program"; + const char* program_value = "GLFW research prototype"; + const char* display_key = "display"; + char display_value[256]; + const char* display_value_string = display_value; + + snprintf(display_value, sizeof(display_value), "x11:%s", + getenv("DISPLAY") ? getenv("DISPLAY") : ""); + + dbus_message_iter_init_append(message, &iter); + if (!dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "(ss)", &array)) + { + dbus_message_unref(message); + disconnect_fcitx5(backend, "CreateInputContext argument open failed"); + return GLFW_FALSE; + } + + if (!dbus_message_iter_open_container(&array, DBUS_TYPE_STRUCT, NULL, &structure) || + !dbus_message_iter_append_basic(&structure, DBUS_TYPE_STRING, &program_key) || + !dbus_message_iter_append_basic(&structure, DBUS_TYPE_STRING, &program_value) || + !dbus_message_iter_close_container(&array, &structure) || + !dbus_message_iter_open_container(&array, DBUS_TYPE_STRUCT, NULL, &structure) || + !dbus_message_iter_append_basic(&structure, DBUS_TYPE_STRING, &display_key) || + !dbus_message_iter_append_basic(&structure, DBUS_TYPE_STRING, &display_value_string) || + !dbus_message_iter_close_container(&array, &structure)) + { + dbus_message_unref(message); + disconnect_fcitx5(backend, "CreateInputContext argument append failed"); + return GLFW_FALSE; + } + + dbus_message_iter_close_container(&iter, &array); + } + + 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); + disconnect_fcitx5(backend, "CreateInputContext failed"); + return GLFW_FALSE; + } + + if (!parse_create_input_context_reply(reply, &path)) + { + log_line(backend, "CreateInputContext reply parse failed"); + dbus_message_unref(reply); + disconnect_fcitx5(backend, "CreateInputContext reply parse failed"); + return GLFW_FALSE; + } + + backend->input_context_path = xstrdup(path); + dbus_message_unref(reply); + if (!backend->input_context_path) + { + disconnect_fcitx5(backend, "input context path allocation failed"); + return GLFW_FALSE; + } + + log_line(backend, "created Fcitx5 input context path=%s", + backend->input_context_path ? backend->input_context_path : ""); + + dbus_error_init(&error); + dbus_bus_add_match(backend->connection, + "type='signal',interface='org.fcitx.Fcitx.InputContext1'", + &error); + if (dbus_error_is_set(&error)) + { + log_line(backend, "failed to add Fcitx5 signal match: %s", + error.message ? error.message : ""); + dbus_error_free(&error); + disconnect_fcitx5(backend, "signal match failed"); + return GLFW_FALSE; + } + + if (!dbus_connection_add_filter(backend->connection, dbus_filter, backend, NULL)) + { + disconnect_fcitx5(backend, "signal filter failed"); + return GLFW_FALSE; + } + backend->filter_added = GLFW_TRUE; + log_line(backend, "setting Fcitx5 capabilities=0x%llx", + (unsigned long long) caps); + if (!call_no_reply(backend, "SetCapability", + DBUS_TYPE_UINT64, &caps, + DBUS_TYPE_INVALID)) + { + disconnect_fcitx5(backend, "SetCapability failed"); + return GLFW_FALSE; + } + backend->ready = GLFW_TRUE; + backend->status = GLFW_TRUE; + backend->reconnect_at = 0.0; + + log_line(backend, "connected to Fcitx5 path=%s", + backend->input_context_path ? backend->input_context_path : ""); + + if (backend->focused_window && + !call_no_reply(backend, "FocusIn", DBUS_TYPE_INVALID)) + { + disconnect_fcitx5(backend, "restored FocusIn failed"); + return GLFW_FALSE; + } + + 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 x11_keycode = request->event.keycode; + dbus_uint32_t keycode = x11_keycode; + dbus_uint32_t state = fcitx5_state_from_glfw(request->event.mods); + dbus_bool_t is_release = request->event.action == GLFW_RELEASE; + dbus_uint32_t time = request->event.time; + int disconnect_after_request = GLFW_FALSE; + + 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 x11_keycode=%u fcitx5_keycode=%u state=0x%x release=%i", + request->id, request->event.time, request->queued_at, + keyval, x11_keycode, keycode, state, is_release); + + 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, "SetCursorRect", + 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(FCITX5_SERVICE, + backend->input_context_path, + FCITX5_INPUT_INTERFACE, + "ProcessKeyEvent"); + if (message && + dbus_message_append_args(message, + DBUS_TYPE_UINT32, &keyval, + DBUS_TYPE_UINT32, &keycode, + DBUS_TYPE_UINT32, &state, + DBUS_TYPE_BOOLEAN, &is_release, + DBUS_TYPE_UINT32, &time, + 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; + disconnect_after_request = GLFW_TRUE; + } + } + else + { + request->failed = GLFW_TRUE; + disconnect_after_request = 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; + disconnect_after_request = 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); + + if (disconnect_after_request) + disconnect_fcitx5(backend, "ProcessKeyEvent failed"); +} + +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) + { + } + + if (!dbus_connection_get_is_connected(backend->connection)) + disconnect_fcitx5(backend, "D-Bus connection disconnected"); +} + +static void handle_unready_command(GLFWx11IMEBackend* backend, Command* command) +{ + switch (command->type) + { + case COMMAND_FOCUS_IN: + backend->focused_window = command->window; + break; + case COMMAND_FOCUS_OUT: + if (backend->focused_window == command->window) + backend->focused_window = NULL; + break; + case COMMAND_SET_STATUS: + backend->status = command->status; + break; + case COMMAND_KEY: + complete_failed_request(backend, command->request); + command->request = NULL; + break; + default: + break; + } +} + +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. + for (;;) + { + Command* command = NULL; + + if (backend->running && !backend->ready) + { + const double now = now_seconds(backend); + if (backend->reconnect_at <= 0.0 || now >= backend->reconnect_at) + connect_fcitx5(backend); + } + + 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) + { + handle_unready_command(backend, command); + 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, + "SetCursorRect final=(%i,%i %ix%i)", + command->x, command->y, command->w, command->h); + call_no_reply(backend, "SetCursorRect", + 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); + } + + disconnect_fcitx5(backend, "worker shutdown"); + 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->status = GLFW_TRUE; + backend->timeout_ms = 100.0; + backend->reconnect_interval = 1.0; + timeout = getenv("GLFW_FCITX5_TIMEOUT_MS"); + if (timeout && *timeout) + backend->timeout_ms = atof(timeout); + + dbus_threads_init_default(); + + 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->block_sizes); + 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 caret=%i blocks=%i focused=%i text='%s'", + event->type, event->request_id, event->request_timed_out, + event->timestamp, event->caret, event->block_count, + event->focused_block, 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, + event->block_sizes, + event->block_count, + event->focused_block); + } + 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->block_sizes); + 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; +}