aboutsummaryrefslogtreecommitdiff
path: root/backend-windows.c
diff options
context:
space:
mode:
authorLena <lena@omega>2026-01-01 00:00:00 +0000
committerLena <lena@omega>2026-01-01 00:00:00 +0000
commit2fa72ee0dc67d973330aec4ba98260e9f57f7821 (patch)
treee6a25c07c8da61d29e4e1f8a72436565e63c5777 /backend-windows.c
downloadmouseboard-0.1.tar.gz
Enter mouseboardHEADv0.1master
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.
Diffstat (limited to 'backend-windows.c')
-rw-r--r--backend-windows.c403
1 files changed, 403 insertions, 0 deletions
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 <windows.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#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;
+}