diff --git a/README.md b/README.md index 20f3fa6..6bd50ca 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # axidev-io -`axidev-io` is a C library for keyboard input injection and global keyboard -listening on Windows and Linux. +`axidev-io` is a C library for keyboard input injection, global keyboard +listening, and mouse cursor/button observation on Windows and Linux. ## Build @@ -62,12 +62,36 @@ int main(void) { } ``` +Mouse polling and listening use the same single global runtime: + +```c +static void on_mouse(const axidev_io_mouse_state_t *state, void *user_data) { + printf("Mouse: x=%d, y=%d, buttons=%u\n", + state->x, state->y, state->buttons); +} + +int main(void) { + axidev_io_mouse_state_t state; + axidev_io_mouse_poll(&state); + + if (!axidev_io_mouse_listener_start(on_mouse, NULL)) { + return 1; + } + + axidev_io_mouse_listener_stop(); + return 0; +} +``` + ## Notes - Normal sender calls do not take sender handles, listener handles, or context handles. - Shared runtime state lives in the single exported `axidev_io_global` pointer. +- Mouse callbacks report cursor/button state snapshots. On Linux, libinput + does not expose the compositor cursor position directly, so the library tracks + cursor position from observed pointer events. - Printable text sending prefers layout-resolved key sequences; explicit `key_down`, `key_up`, and `tap` remain available for low-level control. - `axidev_io_keyboard_key_down(key_mod, true)` requests backend-managed repeat diff --git a/build.py b/build.py index e694cba..a585744 100644 --- a/build.py +++ b/build.py @@ -32,6 +32,7 @@ Path("src/core/log.c"), Path("src/internal/utf.c"), Path("src/vendor/stb_ds_impl.c"), + Path("src/mouse/mouse_common.c"), Path("src/keyboard/common/key_utils.c"), Path("src/keyboard/common/keymap.c"), ] @@ -158,6 +159,7 @@ def make_config(build_dir: Path) -> BuildConfig: Path("src/keyboard/common/windows_keymap.c"), Path("src/keyboard/sender/sender_windows.c"), Path("src/keyboard/listener/listener_windows.c"), + Path("src/mouse/mouse_windows.c"), ] ) platform_libs.extend(["-luser32", "-lkernel32"]) @@ -189,6 +191,7 @@ def make_config(build_dir: Path) -> BuildConfig: Path("src/keyboard/common/linux_keysym.c"), Path("src/keyboard/sender/sender_uinput.c"), Path("src/keyboard/listener/listener_linux.c"), + Path("src/mouse/mouse_linux.c"), ] ) diff --git a/docs/consumers/README.md b/docs/consumers/README.md index 08c2cb9..aad1b56 100644 --- a/docs/consumers/README.md +++ b/docs/consumers/README.md @@ -173,6 +173,19 @@ Example: - Callbacks may run on an internal background thread. - Keep listener callbacks thread-safe and short. +## Mouse + +- `axidev_io_mouse_poll()` returns the latest cursor/button snapshot. +- `axidev_io_mouse_listener_start()` starts the single global mouse listener. +- Mouse callbacks receive an `axidev_io_mouse_state_t` with cursor position, + button bitmask, scroll deltas for the current event, and a monotonic + timestamp. +- On Windows, polling reads the current cursor and button state from Win32. +- On Linux, listening uses libinput on `seat0` and updates the tracked state + from pointer events. libinput does not expose the desktop compositor cursor + position directly, so polling returns the latest tracked state rather than + querying a compositor-specific pointer API. + ## Errors And Logging - Failure details are available through `axidev_io_get_last_error()`. @@ -183,7 +196,9 @@ Example: ## Platform Notes - Windows uses the Win32 keyboard APIs for injection and a low-level hook for - listening. + listening. Mouse observation uses Win32 cursor/button APIs and a low-level + mouse hook. - Linux injection uses `uinput`. -- Linux listening uses `libinput` plus `xkbcommon`. +- Linux keyboard listening uses `libinput` plus `xkbcommon`; mouse listening + uses `libinput`. - macOS is not supported in this repository. diff --git a/include/axidev-io/c_api.h b/include/axidev-io/c_api.h index ac053bb..43a7ed5 100644 --- a/include/axidev-io/c_api.h +++ b/include/axidev-io/c_api.h @@ -38,6 +38,7 @@ extern "C" { #define AXIDEV_IO_KEYBOARD_SENDER_STORAGE_SIZE 4096u #define AXIDEV_IO_KEYBOARD_LISTENER_STORAGE_SIZE 16384u #define AXIDEV_IO_KEYBOARD_KEYMAP_STORAGE_SIZE 8192u +#define AXIDEV_IO_MOUSE_STORAGE_SIZE 4096u #define AXIDEV_IO_GLOBAL_PRIVATE_STORAGE_SIZE 2048u typedef union axidev_io_keyboard_sender_storage_t { @@ -55,6 +56,11 @@ typedef union axidev_io_keyboard_keymap_storage_t { unsigned char bytes[AXIDEV_IO_KEYBOARD_KEYMAP_STORAGE_SIZE]; } axidev_io_keyboard_keymap_storage_t; +typedef union axidev_io_mouse_storage_t { + max_align_t _align; + unsigned char bytes[AXIDEV_IO_MOUSE_STORAGE_SIZE]; +} axidev_io_mouse_storage_t; + typedef union axidev_io_global_private_storage_t { max_align_t _align; unsigned char bytes[AXIDEV_IO_GLOBAL_PRIVATE_STORAGE_SIZE]; @@ -356,6 +362,29 @@ typedef void (*axidev_io_keyboard_listener_cb)( uint32_t codepoint, axidev_io_keyboard_key_with_modifier_t key_mod, bool pressed, void *user_data); +typedef uint16_t axidev_io_mouse_button_t; + +enum { + AXIDEV_IO_MOUSE_BUTTON_NONE = 0x0000, + AXIDEV_IO_MOUSE_BUTTON_LEFT = 0x0001, + AXIDEV_IO_MOUSE_BUTTON_RIGHT = 0x0002, + AXIDEV_IO_MOUSE_BUTTON_MIDDLE = 0x0004, + AXIDEV_IO_MOUSE_BUTTON_BACK = 0x0008, + AXIDEV_IO_MOUSE_BUTTON_FORWARD = 0x0010 +}; + +typedef struct axidev_io_mouse_state_t { + int32_t x; + int32_t y; + axidev_io_mouse_button_t buttons; + double scroll_x; + double scroll_y; + uint64_t timestamp_ms; +} axidev_io_mouse_state_t; + +typedef void (*axidev_io_mouse_listener_cb)( + const axidev_io_mouse_state_t *state, void *user_data); + typedef struct axidev_io_keyboard_sender_context { bool initialized; bool ready; @@ -386,6 +415,14 @@ typedef struct axidev_io_keyboard_context { axidev_io_keyboard_keymap_context keymap; } axidev_io_keyboard_context; +typedef struct axidev_io_mouse_context { + bool initialized; + bool is_listening; + axidev_io_keyboard_backend_type_t backend_type; + axidev_io_mouse_state_t state; + axidev_io_mouse_storage_t storage; +} axidev_io_mouse_context; + typedef uint8_t axidev_io_log_level_t; enum { @@ -399,6 +436,7 @@ typedef struct axidev_io_global_context { axidev_io_log_level_t log_level; char *last_error; axidev_io_keyboard_context keyboard; + axidev_io_mouse_context mouse; axidev_io_global_private_storage_t private_storage; } axidev_io_global_context; @@ -435,6 +473,13 @@ AXIDEV_IO_API bool axidev_io_listener_start(axidev_io_keyboard_listener_cb cb, AXIDEV_IO_API void axidev_io_listener_stop(void); AXIDEV_IO_API bool axidev_io_listener_is_listening(void); +AXIDEV_IO_API bool axidev_io_mouse_poll(axidev_io_mouse_state_t *out_state); +AXIDEV_IO_API bool +axidev_io_mouse_listener_start(axidev_io_mouse_listener_cb cb, + void *user_data); +AXIDEV_IO_API void axidev_io_mouse_listener_stop(void); +AXIDEV_IO_API bool axidev_io_mouse_listener_is_listening(void); + AXIDEV_IO_API char * axidev_io_keyboard_key_to_string(axidev_io_keyboard_key_t key); AXIDEV_IO_API axidev_io_keyboard_key_t diff --git a/src/c_api.c b/src/c_api.c index 47eee33..4b1fdfe 100644 --- a/src/c_api.c +++ b/src/c_api.c @@ -11,6 +11,7 @@ #include "keyboard/common/keymap_internal.h" #include "keyboard/listener/listener_internal.h" #include "keyboard/sender/sender_internal.h" +#include "mouse/mouse_internal.h" static void axidev_io_report_result(const char *function_name, axidev_io_result result) { @@ -440,6 +441,54 @@ AXIDEV_IO_API bool axidev_io_listener_is_listening(void) { return axidev_io_global->keyboard.listener.is_listening; } +AXIDEV_IO_API bool axidev_io_mouse_poll(axidev_io_mouse_state_t *out_state) { + axidev_io_result result; + + axidev_io_context_ensure_runtime(); + axidev_io_clear_last_error_internal(); + if (out_state == NULL) { + axidev_io_report_result("axidev_io_mouse_poll", + AXIDEV_IO_RESULT_INVALID_ARGUMENT); + return false; + } + + axidev_io_context_lock(); + result = axidev_io_mouse_poll_internal(out_state); + if (result != AXIDEV_IO_RESULT_OK) { + axidev_io_report_result("axidev_io_mouse_poll", result); + } + axidev_io_context_unlock(); + return result == AXIDEV_IO_RESULT_OK; +} + +AXIDEV_IO_API bool +axidev_io_mouse_listener_start(axidev_io_mouse_listener_cb cb, + void *user_data) { + axidev_io_result result; + + axidev_io_context_ensure_runtime(); + axidev_io_clear_last_error_internal(); + axidev_io_context_lock(); + result = axidev_io_mouse_listener_start_internal(cb, user_data); + if (result != AXIDEV_IO_RESULT_OK) { + axidev_io_report_result("axidev_io_mouse_listener_start", result); + } + axidev_io_context_unlock(); + return result == AXIDEV_IO_RESULT_OK; +} + +AXIDEV_IO_API void axidev_io_mouse_listener_stop(void) { + axidev_io_context_ensure_runtime(); + axidev_io_context_lock(); + axidev_io_mouse_listener_stop_internal(); + axidev_io_context_unlock(); +} + +AXIDEV_IO_API bool axidev_io_mouse_listener_is_listening(void) { + axidev_io_context_ensure_runtime(); + return axidev_io_global->mouse.is_listening; +} + AXIDEV_IO_API char * axidev_io_keyboard_key_to_string(axidev_io_keyboard_key_t key) { axidev_io_context_ensure_runtime(); diff --git a/src/core/context.c b/src/core/context.c index aad6595..3b5a477 100644 --- a/src/core/context.c +++ b/src/core/context.c @@ -21,6 +21,7 @@ static void axidev_io_context_init_once(void) { axidev_io_mutex_init(&runtime->error_lock); axidev_io_global->log_level = AXIDEV_IO_LOG_LEVEL_INFO; axidev_io_keyboard_reset_public_state(); + axidev_io_mouse_reset_public_state(); } void axidev_io_context_ensure_runtime(void) { @@ -161,3 +162,8 @@ void axidev_io_keyboard_reset_public_state(void) { axidev_io_keyboard_reset_public_keymap_state(); axidev_io_global->keyboard.backend_type = AXIDEV_IO_BACKEND_UNKNOWN; } + +void axidev_io_mouse_reset_public_state(void) { + memset(&axidev_io_global->mouse, 0, sizeof(axidev_io_global->mouse)); + axidev_io_global->mouse.backend_type = AXIDEV_IO_BACKEND_UNKNOWN; +} diff --git a/src/internal/context.h b/src/internal/context.h index 33a9179..14230ad 100644 --- a/src/internal/context.h +++ b/src/internal/context.h @@ -35,6 +35,7 @@ void axidev_io_keyboard_reset_public_sender_state(void); void axidev_io_keyboard_reset_public_listener_state(void); void axidev_io_keyboard_reset_public_keymap_state(void); void axidev_io_keyboard_reset_public_state(void); +void axidev_io_mouse_reset_public_state(void); static inline void *axidev_io_sender_storage_ptr(void) { return (void *)axidev_io_global->keyboard.sender.storage.bytes; @@ -48,6 +49,10 @@ static inline void *axidev_io_keymap_storage_ptr(void) { return (void *)axidev_io_global->keyboard.keymap.storage.bytes; } +static inline void *axidev_io_mouse_storage_ptr(void) { + return (void *)axidev_io_global->mouse.storage.bytes; +} + static inline axidev_io_keyboard_sender_context * axidev_io_sender_public_context(void) { return &axidev_io_global->keyboard.sender; @@ -63,6 +68,10 @@ axidev_io_keymap_public_context(void) { return &axidev_io_global->keyboard.keymap; } +static inline axidev_io_mouse_context *axidev_io_mouse_public_context(void) { + return &axidev_io_global->mouse; +} + static inline bool axidev_io_keyboard_has_modifier(axidev_io_keyboard_modifier_t state, axidev_io_keyboard_modifier_t flag) { diff --git a/src/mouse/mouse_common.c b/src/mouse/mouse_common.c new file mode 100644 index 0000000..802e94b --- /dev/null +++ b/src/mouse/mouse_common.c @@ -0,0 +1,71 @@ +#include "mouse_internal.h" + +axidev_io_mouse_impl *axidev_io_mouse_impl_get(void) { + return (axidev_io_mouse_impl *)axidev_io_mouse_storage_ptr(); +} + +bool axidev_io_mouse_prepare_locks(axidev_io_mouse_impl *impl) { + if (impl == NULL) { + return false; + } + if (!impl->callback_lock_ready) { + if (!axidev_io_mutex_init(&impl->callback_lock)) { + return false; + } + impl->callback_lock_ready = true; + } + if (!impl->state_lock_ready) { + if (!axidev_io_mutex_init(&impl->state_lock)) { + return false; + } + impl->state_lock_ready = true; + } + return true; +} + +void axidev_io_mouse_store_state(axidev_io_mouse_impl *impl, + const axidev_io_mouse_state_t *state) { + if (impl == NULL || state == NULL) { + return; + } + if (impl->state_lock_ready) { + axidev_io_mutex_lock(&impl->state_lock); + } + axidev_io_mouse_public_context()->state = *state; + if (impl->state_lock_ready) { + axidev_io_mutex_unlock(&impl->state_lock); + } +} + +void axidev_io_mouse_load_state(axidev_io_mouse_impl *impl, + axidev_io_mouse_state_t *out_state) { + if (out_state == NULL) { + return; + } + if (impl != NULL && impl->state_lock_ready) { + axidev_io_mutex_lock(&impl->state_lock); + } + *out_state = axidev_io_mouse_public_context()->state; + if (impl != NULL && impl->state_lock_ready) { + axidev_io_mutex_unlock(&impl->state_lock); + } +} + +void axidev_io_mouse_invoke_callback(axidev_io_mouse_impl *impl, + const axidev_io_mouse_state_t *state) { + axidev_io_mouse_listener_cb callback = NULL; + void *user_data = NULL; + + if (impl == NULL || state == NULL) { + return; + } + + axidev_io_mutex_lock(&impl->callback_lock); + callback = impl->callback; + user_data = impl->user_data; + axidev_io_mutex_unlock(&impl->callback_lock); + + if (callback != NULL) { + callback(state, user_data); + } +} diff --git a/src/mouse/mouse_internal.h b/src/mouse/mouse_internal.h new file mode 100644 index 0000000..d8f7d2c --- /dev/null +++ b/src/mouse/mouse_internal.h @@ -0,0 +1,46 @@ +#pragma once +#ifndef AXIDEV_IO_MOUSE_INTERNAL_H +#define AXIDEV_IO_MOUSE_INTERNAL_H + +#include "../internal/context.h" + +#include + +typedef struct axidev_io_mouse_impl { + axidev_io_mouse_listener_cb callback; + void *user_data; + axidev_io_mutex callback_lock; + axidev_io_mutex state_lock; + bool callback_lock_ready; + bool state_lock_ready; + axidev_io_thread worker; + atomic_bool running; + atomic_bool ready; +#ifdef _WIN32 + void *hook; + uint32_t thread_id; +#elif defined(__linux__) + struct axidev_io_linux_mouse_platform *platform; +#endif +} axidev_io_mouse_impl; + +_Static_assert(sizeof(axidev_io_mouse_impl) <= AXIDEV_IO_MOUSE_STORAGE_SIZE, + "mouse storage is too small"); + +axidev_io_mouse_impl *axidev_io_mouse_impl_get(void); + +axidev_io_result axidev_io_mouse_poll_internal( + axidev_io_mouse_state_t *out_state); +axidev_io_result axidev_io_mouse_listener_start_internal( + axidev_io_mouse_listener_cb callback, void *user_data); +void axidev_io_mouse_listener_stop_internal(void); + +void axidev_io_mouse_store_state(axidev_io_mouse_impl *impl, + const axidev_io_mouse_state_t *state); +void axidev_io_mouse_load_state(axidev_io_mouse_impl *impl, + axidev_io_mouse_state_t *out_state); +void axidev_io_mouse_invoke_callback(axidev_io_mouse_impl *impl, + const axidev_io_mouse_state_t *state); +bool axidev_io_mouse_prepare_locks(axidev_io_mouse_impl *impl); + +#endif diff --git a/src/mouse/mouse_linux.c b/src/mouse/mouse_linux.c new file mode 100644 index 0000000..e9d7882 --- /dev/null +++ b/src/mouse/mouse_linux.c @@ -0,0 +1,278 @@ +#if defined(__linux__) + +#include "mouse_internal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +struct axidev_io_linux_mouse_platform { + struct libinput *libinput; + struct udev *udev; +}; + +static int axidev_io_mouse_open_restricted(const char *path, int flags, + void *user_data) { + (void)user_data; + { + int fd = open(path, flags); + return fd < 0 ? -errno : fd; + } +} + +static void axidev_io_mouse_close_restricted(int fd, void *user_data) { + (void)user_data; + close(fd); +} + +static const struct libinput_interface g_mouse_libinput_interface = { + axidev_io_mouse_open_restricted, axidev_io_mouse_close_restricted}; + +static axidev_io_mouse_button_t +axidev_io_linux_button_flag(uint32_t button) { + switch (button) { + case BTN_LEFT: + return AXIDEV_IO_MOUSE_BUTTON_LEFT; + case BTN_RIGHT: + return AXIDEV_IO_MOUSE_BUTTON_RIGHT; + case BTN_MIDDLE: + return AXIDEV_IO_MOUSE_BUTTON_MIDDLE; + case BTN_BACK: + case BTN_SIDE: + return AXIDEV_IO_MOUSE_BUTTON_BACK; + case BTN_FORWARD: + case BTN_EXTRA: + return AXIDEV_IO_MOUSE_BUTTON_FORWARD; + default: + return AXIDEV_IO_MOUSE_BUTTON_NONE; + } +} + +static void axidev_io_linux_mouse_close( + struct axidev_io_linux_mouse_platform *platform) { + if (platform == NULL) { + return; + } + if (platform->libinput != NULL) { + libinput_unref(platform->libinput); + platform->libinput = NULL; + } + if (platform->udev != NULL) { + udev_unref(platform->udev); + platform->udev = NULL; + } +} + +static axidev_io_result axidev_io_linux_mouse_open( + struct axidev_io_linux_mouse_platform *platform) { + if (platform == NULL) { + return AXIDEV_IO_RESULT_INVALID_ARGUMENT; + } + platform->udev = udev_new(); + if (platform->udev == NULL) { + return AXIDEV_IO_RESULT_PLATFORM_ERROR; + } + platform->libinput = + libinput_udev_create_context(&g_mouse_libinput_interface, NULL, + platform->udev); + if (platform->libinput == NULL || + libinput_udev_assign_seat(platform->libinput, "seat0") < 0) { + axidev_io_linux_mouse_close(platform); + return AXIDEV_IO_RESULT_PLATFORM_ERROR; + } + return AXIDEV_IO_RESULT_OK; +} + +static void axidev_io_linux_mouse_handle_event( + axidev_io_mouse_impl *impl, struct libinput_event *event) { + axidev_io_mouse_state_t state; + enum libinput_event_type type; + bool changed = false; + + if (impl == NULL || event == NULL) { + return; + } + + axidev_io_mouse_load_state(impl, &state); + state.scroll_x = 0.0; + state.scroll_y = 0.0; + state.timestamp_ms = axidev_io_monotonic_time_ms(); + + type = libinput_event_get_type(event); + if (type == LIBINPUT_EVENT_POINTER_MOTION) { + struct libinput_event_pointer *pointer = + libinput_event_get_pointer_event(event); + state.x += (int32_t)libinput_event_pointer_get_dx(pointer); + state.y += (int32_t)libinput_event_pointer_get_dy(pointer); + changed = true; + } else if (type == LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE) { + struct libinput_event_pointer *pointer = + libinput_event_get_pointer_event(event); + state.x = (int32_t)libinput_event_pointer_get_absolute_x_transformed( + pointer, 1000000); + state.y = (int32_t)libinput_event_pointer_get_absolute_y_transformed( + pointer, 1000000); + changed = true; + } else if (type == LIBINPUT_EVENT_POINTER_BUTTON) { + struct libinput_event_pointer *pointer = + libinput_event_get_pointer_event(event); + axidev_io_mouse_button_t flag = + axidev_io_linux_button_flag(libinput_event_pointer_get_button(pointer)); + if (flag != AXIDEV_IO_MOUSE_BUTTON_NONE) { + if (libinput_event_pointer_get_button_state(pointer) == + LIBINPUT_BUTTON_STATE_PRESSED) { + state.buttons |= flag; + } else { + state.buttons = (axidev_io_mouse_button_t)(state.buttons & ~flag); + } + changed = true; + } + } else if (type == LIBINPUT_EVENT_POINTER_AXIS) { + struct libinput_event_pointer *pointer = + libinput_event_get_pointer_event(event); + if (libinput_event_pointer_has_axis(pointer, + LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL)) { + state.scroll_y = libinput_event_pointer_get_axis_value( + pointer, LIBINPUT_POINTER_AXIS_SCROLL_VERTICAL); + changed = true; + } + if (libinput_event_pointer_has_axis(pointer, + LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL)) { + state.scroll_x = libinput_event_pointer_get_axis_value( + pointer, LIBINPUT_POINTER_AXIS_SCROLL_HORIZONTAL); + changed = true; + } + } + + if (changed) { + axidev_io_mouse_store_state(impl, &state); + axidev_io_mouse_invoke_callback(impl, &state); + } +} + +axidev_io_result axidev_io_mouse_poll_internal( + axidev_io_mouse_state_t *out_state) { + axidev_io_mouse_impl *impl = axidev_io_mouse_impl_get(); + + if (out_state == NULL) { + return AXIDEV_IO_RESULT_INVALID_ARGUMENT; + } + if (!axidev_io_mouse_prepare_locks(impl)) { + return AXIDEV_IO_RESULT_INTERNAL_ERROR; + } + axidev_io_mouse_load_state(impl, out_state); + out_state->timestamp_ms = axidev_io_monotonic_time_ms(); + axidev_io_mouse_public_context()->initialized = true; + axidev_io_mouse_public_context()->backend_type = + AXIDEV_IO_BACKEND_LINUX_LIBINPUT; + return AXIDEV_IO_RESULT_OK; +} + +static int axidev_io_mouse_thread_main(void *user_data) { + axidev_io_mouse_impl *impl = (axidev_io_mouse_impl *)user_data; + struct axidev_io_linux_mouse_platform *platform = impl->platform; + struct pollfd poll_fd; + + if (platform == NULL || + axidev_io_linux_mouse_open(platform) != AXIDEV_IO_RESULT_OK) { + atomic_store(&impl->running, false); + return 1; + } + + poll_fd.fd = libinput_get_fd(platform->libinput); + poll_fd.events = POLLIN; + poll_fd.revents = 0; + atomic_store(&impl->ready, true); + + while (atomic_load(&impl->running)) { + int poll_result = poll(&poll_fd, 1, 100); + if (poll_result > 0 && (poll_fd.revents & POLLIN)) { + struct libinput_event *event; + libinput_dispatch(platform->libinput); + while ((event = libinput_get_event(platform->libinput)) != NULL) { + axidev_io_linux_mouse_handle_event(impl, event); + libinput_event_destroy(event); + } + } + axidev_io_sleep_ms(1); + } + + axidev_io_linux_mouse_close(platform); + atomic_store(&impl->ready, false); + return 0; +} + +axidev_io_result axidev_io_mouse_listener_start_internal( + axidev_io_mouse_listener_cb callback, void *user_data) { + axidev_io_mouse_impl *impl = axidev_io_mouse_impl_get(); + + if (callback == NULL) { + return AXIDEV_IO_RESULT_INVALID_ARGUMENT; + } + if (!axidev_io_mouse_prepare_locks(impl)) { + return AXIDEV_IO_RESULT_INTERNAL_ERROR; + } + if (atomic_load(&impl->running)) { + return AXIDEV_IO_RESULT_ALREADY_INITIALIZED; + } + if (impl->platform == NULL) { + impl->platform = (struct axidev_io_linux_mouse_platform *)calloc( + 1, sizeof(*impl->platform)); + if (impl->platform == NULL) { + return AXIDEV_IO_RESULT_INTERNAL_ERROR; + } + } + + axidev_io_mutex_lock(&impl->callback_lock); + impl->callback = callback; + impl->user_data = user_data; + axidev_io_mutex_unlock(&impl->callback_lock); + + atomic_store(&impl->running, true); + atomic_store(&impl->ready, false); + if (!axidev_io_thread_create(&impl->worker, axidev_io_mouse_thread_main, + impl)) { + atomic_store(&impl->running, false); + return AXIDEV_IO_RESULT_PLATFORM_ERROR; + } + + for (int i = 0; i < 40; ++i) { + if (!atomic_load(&impl->running)) { + axidev_io_thread_join(&impl->worker); + return AXIDEV_IO_RESULT_PLATFORM_ERROR; + } + if (atomic_load(&impl->ready)) { + axidev_io_mouse_public_context()->initialized = true; + axidev_io_mouse_public_context()->is_listening = true; + axidev_io_mouse_public_context()->backend_type = + AXIDEV_IO_BACKEND_LINUX_LIBINPUT; + return AXIDEV_IO_RESULT_OK; + } + axidev_io_sleep_ms(5); + } + + atomic_store(&impl->running, false); + axidev_io_thread_join(&impl->worker); + return AXIDEV_IO_RESULT_PLATFORM_ERROR; +} + +void axidev_io_mouse_listener_stop_internal(void) { + axidev_io_mouse_impl *impl = axidev_io_mouse_impl_get(); + + if (!atomic_load(&impl->running)) { + axidev_io_mouse_public_context()->is_listening = false; + return; + } + + atomic_store(&impl->running, false); + axidev_io_thread_join(&impl->worker); + axidev_io_mouse_public_context()->is_listening = false; + axidev_io_mouse_public_context()->initialized = true; +} + +#endif diff --git a/src/mouse/mouse_windows.c b/src/mouse/mouse_windows.c new file mode 100644 index 0000000..771201a --- /dev/null +++ b/src/mouse/mouse_windows.c @@ -0,0 +1,204 @@ +#ifdef _WIN32 + +#include "mouse_internal.h" + +#include + +#include + +static _Atomic(axidev_io_mouse_impl *) g_active_mouse_listener; + +static axidev_io_mouse_button_t axidev_io_windows_mouse_buttons(void) { + axidev_io_mouse_button_t buttons = AXIDEV_IO_MOUSE_BUTTON_NONE; + if (GetAsyncKeyState(VK_LBUTTON) & 0x8000) { + buttons |= AXIDEV_IO_MOUSE_BUTTON_LEFT; + } + if (GetAsyncKeyState(VK_RBUTTON) & 0x8000) { + buttons |= AXIDEV_IO_MOUSE_BUTTON_RIGHT; + } + if (GetAsyncKeyState(VK_MBUTTON) & 0x8000) { + buttons |= AXIDEV_IO_MOUSE_BUTTON_MIDDLE; + } + if (GetAsyncKeyState(VK_XBUTTON1) & 0x8000) { + buttons |= AXIDEV_IO_MOUSE_BUTTON_BACK; + } + if (GetAsyncKeyState(VK_XBUTTON2) & 0x8000) { + buttons |= AXIDEV_IO_MOUSE_BUTTON_FORWARD; + } + return buttons; +} + +static axidev_io_mouse_state_t axidev_io_windows_mouse_snapshot(void) { + POINT point; + axidev_io_mouse_state_t state; + axidev_io_mouse_load_state(axidev_io_mouse_impl_get(), &state); + if (GetCursorPos(&point)) { + state.x = point.x; + state.y = point.y; + } + state.buttons = axidev_io_windows_mouse_buttons(); + state.timestamp_ms = axidev_io_monotonic_time_ms(); + return state; +} + +axidev_io_result axidev_io_mouse_poll_internal( + axidev_io_mouse_state_t *out_state) { + axidev_io_mouse_impl *impl = axidev_io_mouse_impl_get(); + axidev_io_mouse_state_t state; + + if (out_state == NULL) { + return AXIDEV_IO_RESULT_INVALID_ARGUMENT; + } + if (!axidev_io_mouse_prepare_locks(impl)) { + return AXIDEV_IO_RESULT_INTERNAL_ERROR; + } + state = axidev_io_windows_mouse_snapshot(); + axidev_io_mouse_store_state(impl, &state); + *out_state = state; + axidev_io_mouse_public_context()->initialized = true; + axidev_io_mouse_public_context()->backend_type = AXIDEV_IO_BACKEND_WINDOWS; + return AXIDEV_IO_RESULT_OK; +} + +static void +axidev_io_windows_mouse_publish(axidev_io_mouse_impl *impl, + const axidev_io_mouse_state_t *state) { + axidev_io_mouse_store_state(impl, state); + axidev_io_mouse_invoke_callback(impl, state); +} + +static LRESULT CALLBACK axidev_io_low_level_mouse_proc(int nCode, + WPARAM wParam, + LPARAM lParam) { + axidev_io_mouse_impl *impl; + const MSLLHOOKSTRUCT *mouse; + axidev_io_mouse_state_t state; + + if (nCode < 0) { + return CallNextHookEx(NULL, nCode, wParam, lParam); + } + + impl = atomic_load(&g_active_mouse_listener); + if (impl == NULL) { + return CallNextHookEx(NULL, nCode, wParam, lParam); + } + + mouse = (const MSLLHOOKSTRUCT *)lParam; + axidev_io_mouse_load_state(impl, &state); + state.x = mouse->pt.x; + state.y = mouse->pt.y; + state.buttons = axidev_io_windows_mouse_buttons(); + state.scroll_x = 0.0; + state.scroll_y = 0.0; + if (wParam == WM_MOUSEWHEEL) { + state.scroll_y = + (double)(SHORT)HIWORD(mouse->mouseData) / (double)WHEEL_DELTA; + } else if (wParam == WM_MOUSEHWHEEL) { + state.scroll_x = + (double)(SHORT)HIWORD(mouse->mouseData) / (double)WHEEL_DELTA; + } + state.timestamp_ms = axidev_io_monotonic_time_ms(); + axidev_io_windows_mouse_publish(impl, &state); + return CallNextHookEx(NULL, nCode, wParam, lParam); +} + +static int axidev_io_mouse_thread_main(void *user_data) { + axidev_io_mouse_impl *impl = (axidev_io_mouse_impl *)user_data; + MSG message; + axidev_io_mouse_state_t state; + + impl->thread_id = GetCurrentThreadId(); + atomic_store(&g_active_mouse_listener, impl); + state = axidev_io_windows_mouse_snapshot(); + axidev_io_mouse_store_state(impl, &state); + impl->hook = SetWindowsHookEx(WH_MOUSE_LL, axidev_io_low_level_mouse_proc, + GetModuleHandle(NULL), 0); + if (impl->hook == NULL) { + atomic_store(&g_active_mouse_listener, NULL); + impl->thread_id = 0; + atomic_store(&impl->running, false); + atomic_store(&impl->ready, false); + return 1; + } + + atomic_store(&impl->ready, true); + while (GetMessage(&message, NULL, 0, 0) > 0) { + TranslateMessage(&message); + DispatchMessage(&message); + } + + UnhookWindowsHookEx((HHOOK)impl->hook); + impl->hook = NULL; + impl->thread_id = 0; + atomic_store(&impl->ready, false); + atomic_store(&g_active_mouse_listener, NULL); + return 0; +} + +axidev_io_result axidev_io_mouse_listener_start_internal( + axidev_io_mouse_listener_cb callback, void *user_data) { + axidev_io_mouse_impl *impl = axidev_io_mouse_impl_get(); + + if (callback == NULL) { + return AXIDEV_IO_RESULT_INVALID_ARGUMENT; + } + if (!axidev_io_mouse_prepare_locks(impl)) { + return AXIDEV_IO_RESULT_INTERNAL_ERROR; + } + if (atomic_load(&impl->running)) { + return AXIDEV_IO_RESULT_ALREADY_INITIALIZED; + } + + axidev_io_mutex_lock(&impl->callback_lock); + impl->callback = callback; + impl->user_data = user_data; + axidev_io_mutex_unlock(&impl->callback_lock); + + atomic_store(&impl->running, true); + atomic_store(&impl->ready, false); + if (!axidev_io_thread_create(&impl->worker, axidev_io_mouse_thread_main, + impl)) { + atomic_store(&impl->running, false); + return AXIDEV_IO_RESULT_PLATFORM_ERROR; + } + + for (int i = 0; i < 40; ++i) { + if (!atomic_load(&impl->running)) { + axidev_io_thread_join(&impl->worker); + return AXIDEV_IO_RESULT_PLATFORM_ERROR; + } + if (atomic_load(&impl->ready)) { + axidev_io_mouse_public_context()->initialized = true; + axidev_io_mouse_public_context()->is_listening = true; + axidev_io_mouse_public_context()->backend_type = AXIDEV_IO_BACKEND_WINDOWS; + return AXIDEV_IO_RESULT_OK; + } + axidev_io_sleep_ms(5); + } + + atomic_store(&impl->running, false); + if (impl->thread_id != 0) { + PostThreadMessage(impl->thread_id, WM_QUIT, 0, 0); + } + axidev_io_thread_join(&impl->worker); + return AXIDEV_IO_RESULT_PLATFORM_ERROR; +} + +void axidev_io_mouse_listener_stop_internal(void) { + axidev_io_mouse_impl *impl = axidev_io_mouse_impl_get(); + + if (!atomic_load(&impl->running)) { + axidev_io_mouse_public_context()->is_listening = false; + return; + } + + atomic_store(&impl->running, false); + if (impl->thread_id != 0) { + PostThreadMessage(impl->thread_id, WM_QUIT, 0, 0); + } + axidev_io_thread_join(&impl->worker); + axidev_io_mouse_public_context()->is_listening = false; + axidev_io_mouse_public_context()->initialized = true; +} + +#endif diff --git a/tests/test_c_api.c b/tests/test_c_api.c index 99b4cb6..9184fae 100644 --- a/tests/test_c_api.c +++ b/tests/test_c_api.c @@ -26,6 +26,12 @@ static void noop_listener_cb(uint32_t codepoint, (void)user_data; } +static void noop_mouse_listener_cb(const axidev_io_mouse_state_t *state, + void *user_data) { + (void)state; + (void)user_data; +} + static void test_conversion_helpers(void) { char *text; axidev_io_keyboard_key_with_modifier_t parsed; @@ -259,6 +265,38 @@ static void test_listener_lifecycle(void) { } } +static void test_mouse_api_lifecycle(void) { + axidev_io_mouse_state_t state; + char *error_text; + bool started; + + TEST_CHECK(!axidev_io_mouse_poll(NULL)); + error_text = axidev_io_get_last_error(); + TEST_CHECK(error_text != NULL); + axidev_io_free_string(error_text); + + memset(&state, 0, sizeof(state)); + TEST_CHECK(axidev_io_mouse_poll(&state)); + TEST_CHECK(state.timestamp_ms != 0); + + TEST_CHECK(!axidev_io_mouse_listener_start(NULL, NULL)); + error_text = axidev_io_get_last_error(); + TEST_CHECK(error_text != NULL); + axidev_io_free_string(error_text); + + started = axidev_io_mouse_listener_start(noop_mouse_listener_cb, NULL); + if (started) { + TEST_CHECK(axidev_io_mouse_listener_is_listening()); + axidev_io_mouse_listener_stop(); + TEST_CHECK(!axidev_io_mouse_listener_is_listening()); + } else { + error_text = axidev_io_get_last_error(); + if (error_text != NULL) { + axidev_io_free_string(error_text); + } + } +} + int main(void) { TEST_RUN(test_conversion_helpers); #if defined(__linux__) @@ -269,5 +307,6 @@ int main(void) { TEST_RUN(test_windows_repeat_state); #endif TEST_RUN(test_listener_lifecycle); + TEST_RUN(test_mouse_api_lifecycle); return g_axidev_test_failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }