From 2fa72ee0dc67d973330aec4ba98260e9f57f7821 Mon Sep 17 00:00:00 2001 From: Lena Date: Thu, 1 Jan 2026 00:00:00 +0000 Subject: Enter mouseboard Keyboard-only pointing: a labelled grid over every monitor, type a label to jump to a cell, bisect it by quadrant until the crosshair is exact, then one key clicks. The coarse grid keeps labels short and bisection keeps precision unlimited, so a hotkey and a few keystrokes replace the mouse. The core talks only to the backend interface in platform.h; wlroots Wayland and Win32 backends supply the overlay, keyboard grab and pointer. --- backend-windows.c | 403 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 backend-windows.c (limited to 'backend-windows.c') diff --git a/backend-windows.c b/backend-windows.c new file mode 100644 index 0000000..d37c3c4 --- /dev/null +++ b/backend-windows.c @@ -0,0 +1,403 @@ +/* + * backend-windows.c - Win32 backend. + * + * Overlay : one per-monitor layered window (WS_EX_LAYERED) that is + * click-through (WS_EX_TRANSPARENT) and topmost. Its pixels come + * straight from the core's ARGB8888 buffer via UpdateLayeredWindow + * with per-pixel alpha - no conversion, since the buffer is already + * premultiplied BGRA, which is exactly what a 32bpp DIB holds. + * Keyboard : a low-level keyboard hook (WH_KEYBOARD_LL) grabs every key + * globally, so no window focus is needed; consumed keys are + * swallowed. + * Pointer : SendInput with absolute, virtual-desktop coordinates. + */ +#define WINVER 0x0A00 +#define _WIN32_WINNT 0x0A00 +#include "platform.h" + +#include +#include +#include +#include + +#define KQ_SIZE 64 + +struct win_screen { + HWND hwnd; + HDC memdc; + HBITMAP dib; + HGDIOBJ old; /* default bitmap, restored on teardown */ + int x, y, w, h; /* monitor rect, physical pixels */ + struct mb_buffer pub; /* px points straight at the DIB bits */ +}; + +struct win_state { + struct win_screen scr[MB_MAX_SCREENS]; + int n; + HHOOK hook; + DWORD tid; /* thread that owns the hook + message loop */ + uint32_t kq[KQ_SIZE]; /* keys decoded by the hook, drained by next_key */ + int kq_head, kq_tail; +}; + +static struct platform g_platform; +static struct win_state g_ws; + +void platform_sleep_ms(int ms) +{ + Sleep((DWORD)ms); +} + +void platform_early_init(void) +{ + /* GUI-subsystem binary: launching it (e.g. from a hotkey) creates no + * console window. When started from a terminal, attach to its console + * so --help, --dry-run and --selftest still print. */ + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + (void)freopen("CONOUT$", "w", stdout); + (void)freopen("CONOUT$", "w", stderr); + (void)freopen("CONIN$", "r", stdin); + } +} + +/* ---- key queue ------------------------------------------------------- */ + +static void kq_push(uint32_t k) +{ + int n = (g_ws.kq_tail + 1) % KQ_SIZE; + if (n == g_ws.kq_head) + return; /* full; drop */ + g_ws.kq[g_ws.kq_tail] = k; + g_ws.kq_tail = n; +} + +/* ---- keyboard hook --------------------------------------------------- */ + +/* Translate a virtual-key + scancode to an MB_KEY. The few editing keys are + * mapped directly; everything else goes through the active keyboard layout so + * labels follow the user's layout, same as the Wayland xkb path. */ +static uint32_t map_key(DWORD vk, DWORD scan) +{ + switch (vk) { + case VK_ESCAPE: + return MB_KEY_ESC; + case VK_BACK: + return MB_KEY_BACKSPACE; + case VK_RETURN: + return MB_KEY_ENTER; + } + /* GetKeyboardState is stale on a thread that pumps no keyboard input - + * keys arrive only through the LL hook - so sample the modifiers that + * affect translation directly. */ + BYTE ks[256]; + memset(ks, 0, sizeof ks); + if (GetAsyncKeyState(VK_SHIFT) & 0x8000) + ks[VK_SHIFT] = 0x80; + if (GetKeyState(VK_CAPITAL) & 1) + ks[VK_CAPITAL] = 1; + WCHAR buf[8]; + HWND foreground = GetForegroundWindow(); + DWORD tid = foreground ? GetWindowThreadProcessId(foreground, NULL) : 0; + int n = ToUnicodeEx(vk, scan, ks, buf, 8, 0, GetKeyboardLayout(tid)); + if (n == 1 && buf[0] >= 0x20 && buf[0] < 0x7f) + return (uint32_t)buf[0]; + return MB_KEY_NONE; +} + +/* Bare modifiers do nothing on their own, and swallowing their key-ups would + * leave the OS thinking e.g. the launch hotkey's Win key is still held after + * exit - so they pass through the hook untouched. */ +static int is_modifier(DWORD vk) +{ + switch (vk) { + case VK_SHIFT: + case VK_LSHIFT: + case VK_RSHIFT: + case VK_CONTROL: + case VK_LCONTROL: + case VK_RCONTROL: + case VK_MENU: + case VK_LMENU: + case VK_RMENU: + case VK_LWIN: + case VK_RWIN: + case VK_CAPITAL: + return 1; + } + return 0; +} + +static LRESULT CALLBACK ll_hook(int code, WPARAM w, LPARAM l) +{ + if (code == HC_ACTION) { + const KBDLLHOOKSTRUCT *k = (const KBDLLHOOKSTRUCT *)l; + if (is_modifier(k->vkCode)) + return CallNextHookEx(NULL, code, w, l); + if (w == WM_KEYDOWN || w == WM_SYSKEYDOWN) { + uint32_t mb = map_key(k->vkCode, k->scanCode); + if (mb != MB_KEY_NONE) { + kq_push(mb); + /* Wake the GetMessage() in next_key; with no + * focused window the raw key posts no message + * of its own. */ + PostThreadMessage(g_ws.tid, WM_NULL, 0, 0); + } + } + /* Swallow every non-modifier key while the overlay is up: + * nothing leaks to the focused application. */ + return 1; + } + return CallNextHookEx(NULL, code, w, l); +} + +static uint32_t win_next_key(struct platform *p) +{ + (void)p; + for (;;) { + if (g_ws.kq_head != g_ws.kq_tail) { + uint32_t k = g_ws.kq[g_ws.kq_head]; + g_ws.kq_head = (g_ws.kq_head + 1) % KQ_SIZE; + return k; + } + MSG m; + int r = GetMessage(&m, NULL, 0, 0); + if (r <= 0) + return MB_KEY_NONE; /* WM_QUIT or error */ + TranslateMessage(&m); + DispatchMessage(&m); + } +} + +/* ---- frame interface ------------------------------------------------- */ + +static struct mb_buffer *win_frame_begin(struct platform *p, int s) +{ + (void)p; + struct win_screen *w = &g_ws.scr[s]; + memset(w->pub.px, 0, (size_t)w->w * w->h * 4); + return &w->pub; +} + +static int win_frame_commit(struct platform *p) +{ + (void)p; + for (int s = 0; s < g_ws.n; s++) { + struct win_screen *w = &g_ws.scr[s]; + POINT src = { 0, 0 }; + POINT dst = { w->x, w->y }; + SIZE size = { w->w, w->h }; + BLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA }; + if (!UpdateLayeredWindow(w->hwnd, NULL, &dst, &size, w->memdc, + &src, 0, &bf, ULW_ALPHA)) { + fprintf(stderr, "mouseboard: UpdateLayeredWindow failed: %lu\n", + (unsigned long)GetLastError()); + return -1; + } + } + return 0; +} + +/* ---- pointer --------------------------------------------------------- */ + +static int win_pointer_move(struct platform *p, int x, int y) +{ + (void)p; + int vx = GetSystemMetrics(SM_XVIRTUALSCREEN); + int vy = GetSystemMetrics(SM_YVIRTUALSCREEN); + int vw = GetSystemMetrics(SM_CXVIRTUALSCREEN); + int vh = GetSystemMetrics(SM_CYVIRTUALSCREEN); + if (vw < 2) + vw = 2; + if (vh < 2) + vh = 2; + INPUT in; + memset(&in, 0, sizeof in); + in.type = INPUT_MOUSE; + in.mi.dx = (LONG)(((int64_t)(x - vx) * 65535) / (vw - 1)); + in.mi.dy = (LONG)(((int64_t)(y - vy) * 65535) / (vh - 1)); + in.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | + MOUSEEVENTF_VIRTUALDESK; + SetLastError(0); + if (SendInput(1, &in, sizeof in) != 1) { + fprintf(stderr, "mouseboard: SendInput move failed: %lu\n", + (unsigned long)GetLastError()); + return -1; + } + return 0; +} + +static int win_pointer_button(struct platform *p, enum mb_button b, int press) +{ + (void)p; + DWORD f; + if (b == MB_LEFT) + f = press ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP; + else if (b == MB_MIDDLE) + f = press ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP; + else + f = press ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP; + INPUT in; + memset(&in, 0, sizeof in); + in.type = INPUT_MOUSE; + in.mi.dwFlags = f; + SetLastError(0); + if (SendInput(1, &in, sizeof in) != 1) { + fprintf(stderr, "mouseboard: SendInput button failed: %lu\n", + (unsigned long)GetLastError()); + return -1; + } + return 0; +} + +/* ---- window + monitor setup ------------------------------------------ */ + +static LRESULT CALLBACK wndproc(HWND h, UINT m, WPARAM w, LPARAM l) +{ + if (m == WM_DESTROY) { + PostQuitMessage(0); + return 0; + } + return DefWindowProc(h, m, w, l); +} + +static BOOL CALLBACK mon_proc(HMONITOR hm, HDC dc, LPRECT rc, LPARAM lp) +{ + (void)dc; + (void)rc; + (void)lp; + if (g_ws.n >= MB_MAX_SCREENS) + return TRUE; + MONITORINFO mi; + mi.cbSize = sizeof mi; + if (!GetMonitorInfo(hm, &mi)) + return TRUE; + struct win_screen *w = &g_ws.scr[g_ws.n++]; + w->x = mi.rcMonitor.left; + w->y = mi.rcMonitor.top; + w->w = mi.rcMonitor.right - mi.rcMonitor.left; + w->h = mi.rcMonitor.bottom - mi.rcMonitor.top; + return TRUE; +} + +static int screen_setup(struct win_screen *w, HINSTANCE hinst) +{ + w->hwnd = CreateWindowExW(WS_EX_LAYERED | WS_EX_TRANSPARENT | + WS_EX_TOPMOST | WS_EX_TOOLWINDOW | + WS_EX_NOACTIVATE, + L"mouseboard", L"", WS_POPUP, w->x, w->y, w->w, + w->h, NULL, NULL, hinst, NULL); + if (!w->hwnd) + return -1; + + HDC screen = GetDC(NULL); + w->memdc = CreateCompatibleDC(screen); + ReleaseDC(NULL, screen); + if (!w->memdc) + return -1; + + BITMAPINFO bi; + memset(&bi, 0, sizeof bi); + bi.bmiHeader.biSize = sizeof bi.bmiHeader; + bi.bmiHeader.biWidth = w->w; + bi.bmiHeader.biHeight = -w->h; /* top-down: row 0 is the top */ + bi.bmiHeader.biPlanes = 1; + bi.bmiHeader.biBitCount = 32; + bi.bmiHeader.biCompression = BI_RGB; + void *bits = NULL; + w->dib = CreateDIBSection(w->memdc, &bi, DIB_RGB_COLORS, &bits, NULL, 0); + if (!w->dib) + return -1; + w->old = SelectObject(w->memdc, w->dib); + + w->pub.px = bits; + w->pub.w = w->w; + w->pub.h = w->h; + w->pub.stride = w->w; /* device pixels per row */ + w->pub.scale = 1; /* physical pixels: per-monitor DPI aware */ + + ShowWindow(w->hwnd, SW_SHOWNOACTIVATE); + return 0; +} + +/* ---- teardown -------------------------------------------------------- */ + +static void win_teardown(struct platform *p) +{ + (void)p; + if (g_ws.hook) { + UnhookWindowsHookEx(g_ws.hook); + g_ws.hook = NULL; + } + for (int s = 0; s < g_ws.n; s++) { + struct win_screen *w = &g_ws.scr[s]; + if (w->memdc) { + if (w->old) + SelectObject(w->memdc, w->old); + DeleteDC(w->memdc); + } + if (w->dib) + DeleteObject(w->dib); + if (w->hwnd) + DestroyWindow(w->hwnd); + } + g_ws.n = 0; +} + +/* ---- init ------------------------------------------------------------ */ + +struct platform *platform_init(void) +{ + /* Physical pixels everywhere: monitor rects, the overlay buffers and + * the SendInput coordinates then share one coordinate space. */ + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + HINSTANCE hinst = GetModuleHandle(NULL); + WNDCLASSEXW wc; + memset(&wc, 0, sizeof wc); + wc.cbSize = sizeof wc; + wc.lpfnWndProc = wndproc; + wc.hInstance = hinst; + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.lpszClassName = L"mouseboard"; + if (!RegisterClassExW(&wc)) { + fprintf(stderr, "mouseboard: RegisterClassEx failed\n"); + return NULL; + } + + EnumDisplayMonitors(NULL, NULL, mon_proc, 0); + if (g_ws.n == 0) { + fprintf(stderr, "mouseboard: no monitors found\n"); + return NULL; + } + + for (int s = 0; s < g_ws.n; s++) { + if (screen_setup(&g_ws.scr[s], hinst) < 0) { + fprintf(stderr, + "mouseboard: overlay window setup failed\n"); + win_teardown(&g_platform); + return NULL; + } + } + + g_ws.tid = GetCurrentThreadId(); + g_ws.hook = SetWindowsHookEx(WH_KEYBOARD_LL, ll_hook, hinst, 0); + if (!g_ws.hook) { + fprintf(stderr, "mouseboard: cannot install keyboard hook\n"); + win_teardown(&g_platform); + return NULL; + } + + g_platform.priv = &g_ws; + g_platform.n_screens = g_ws.n; + for (int s = 0; s < g_ws.n; s++) + g_platform.screens[s] = (struct mb_screen){ + g_ws.scr[s].x, g_ws.scr[s].y, g_ws.scr[s].w, g_ws.scr[s].h + }; + g_platform.frame_begin = win_frame_begin; + g_platform.frame_commit = win_frame_commit; + g_platform.next_key = win_next_key; + g_platform.pointer_move = win_pointer_move; + g_platform.pointer_button = win_pointer_button; + g_platform.teardown = win_teardown; + return &g_platform; +} -- cgit v1.2.3