/* * 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; }