Files
Tempus3214andClaude Opus 5.5 21c65ec065 Zoom the map with the scroll wheel
On the map the mouse moves the cursor, so it had no way to zoom: the zoom is the right stick's y, which only the
keys and a pad reached. While the map is open, each wheel notch is now a full-tilt push of that stick for 100 ms with
50 ms of neutral after it (a turn away from the player zooms in). A fast scroll's notches wait their turn, at most four,
and a turn the other way drops them. The wheel stays unbound elsewhere.

The back ends count the notches while the cursor is captured (input_win.c from raw input's usButtonData, input_sdl.c
from SDL_EVENT_MOUSE_WHEEL), keeping a high-resolution wheel's part of a notch (ikb_wheel_take).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:48:13 -03:00

253 lines
10 KiB
C

// The Win32 back end of the keyboard and the mouse (input_wsys.h; input.c merges them with the gamepad): Win32 virtual
// keys to the key ids of input_map.h, the mouse camera's raw motion (raw input, WM_INPUT) and the cursor capture.
// Keys and their map: docs/controls.md.
#include <windows.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "input.h"
#include "input_win.h"
#include "input_map.h"
#include "input_wsys.h"
#include "nvn_api.h"
#include "ui/menu.h"
#include "vk.h"
// Win32 virtual key of a key id (the one place that knows them), 0 for an id without a key.
static int key_vk(int key) {
if (key >= KEY_A && key <= KEY_Z) return 'A' + (key - KEY_A);
if (key >= KEY_0 && key <= KEY_9) return '0' + (key - KEY_0);
if (key >= KEY_NUM0 && key <= KEY_NUM9) return VK_NUMPAD0 + (key - KEY_NUM0);
switch (key) {
case KEY_SPACE: return VK_SPACE;
case KEY_TAB: return VK_TAB;
case KEY_LSHIFT: return VK_LSHIFT;
case KEY_LCTRL: return VK_LCONTROL;
case KEY_UP: return VK_UP;
case KEY_DOWN: return VK_DOWN;
case KEY_LEFT: return VK_LEFT;
case KEY_RIGHT: return VK_RIGHT;
case KEY_MOUSE_L: return VK_LBUTTON;
case KEY_MOUSE_R: return VK_RBUTTON;
case KEY_MOUSE_M: return VK_MBUTTON;
case KEY_MOUSE_X1: return VK_XBUTTON1;
case KEY_MOUSE_X2: return VK_XBUTTON2;
case KEY_ENTER: return VK_RETURN;
case KEY_BACKSPACE: return VK_BACK;
case KEY_RSHIFT: return VK_RSHIFT;
case KEY_RCTRL: return VK_RCONTROL;
case KEY_LALT: return VK_LMENU;
case KEY_CAPSLOCK: return VK_CAPITAL;
case KEY_INSERT: return VK_INSERT;
case KEY_DELETE: return VK_DELETE;
case KEY_HOME: return VK_HOME;
case KEY_END: return VK_END;
case KEY_PAGEUP: return VK_PRIOR;
case KEY_PAGEDOWN: return VK_NEXT;
case KEY_F2: return VK_F2;
case KEY_F3: return VK_F3;
case KEY_F4: return VK_F4;
case KEY_F5: return VK_F5;
case KEY_F6: return VK_F6;
case KEY_F7: return VK_F7;
case KEY_F9: return VK_F9;
case KEY_F12: return VK_F12;
case KEY_NUM_ADD: return VK_ADD;
case KEY_NUM_SUB: return VK_SUBTRACT;
case KEY_NUM_MUL: return VK_MULTIPLY;
case KEY_NUM_DIV: return VK_DIVIDE;
case KEY_NUM_DECIMAL: return VK_DECIMAL;
case KEY_MINUS: return VK_OEM_MINUS;
case KEY_EQUALS: return VK_OEM_PLUS;
case KEY_LBRACKET: return VK_OEM_4;
case KEY_RBRACKET: return VK_OEM_6;
case KEY_SEMICOLON: return VK_OEM_1;
case KEY_APOSTROPHE: return VK_OEM_7;
case KEY_COMMA: return VK_OEM_COMMA;
case KEY_PERIOD: return VK_OEM_PERIOD;
case KEY_SLASH: return VK_OEM_2;
case KEY_BACKSLASH: return VK_OEM_5;
case KEY_GRAVE: return VK_OEM_3;
}
return 0;
}
uint32_t input_key_char(int key) {
if (key < KEY_MINUS || key > KEY_GRAVE) return 0;
return MapVirtualKeyW((UINT)key_vk(key), MAPVK_VK_TO_CHAR) & 0x7fffffffu; // the top bit marks a dead key
}
// ---- mouse camera: raw motion accumulated by the window thread, one stick sample per presented frame
static volatile LONG mouse_dx, mouse_dy; // raw counts since the last frame sample
static volatile LONG mouse_travel; // raw counts moved in total, captured or not (ikb_mouse_travel)
static volatile LONG mouse_wheel; // wheel turned while captured, in WHEEL_DELTA units (ikb_wheel_take)
static volatile LONG cur_captured; // cursor hidden and confined to the client area
static volatile LONG cur_active; // WM_ACTIVATEAPP: the window's application is active
// Bits: CUR_REL, Esc or activation by a click on the title bar or border: until a client click; CUR_ESC, set with CUR_REL
// by Esc (not when it was already set): closing the menu gives the capture back. One word, changed by one CAS.
enum { CUR_REL = 1, CUR_ESC = 2 };
static volatile LONG cur_rel;
static void cur_rel_update(LONG (*f)(LONG)) {
LONG o, n;
do { o = cur_rel; n = f(o); } while (n != o && InterlockedCompareExchange(&cur_rel, n, o) != o);
}
static LONG rel_esc(LONG o) { return o & CUR_REL ? o : CUR_REL | CUR_ESC; }
static LONG rel_title(LONG o) { (void)o; return CUR_REL; }
static LONG rel_clear(LONG o) { (void)o; return 0; }
static LONG rel_menu_closed(LONG o) { return o & CUR_ESC ? 0 : o; }
static volatile LONG cur_suspend; // F11 or a modal size or move loop under way
static SRWLOCK cur_lock = SRWLOCK_INIT; // serialises cursor_update (window thread, input_block callers)
// Hide the cursor and confine it to the client area while the window is focused, not suspended, not behind a text entry
// or the menu, and the mouse camera is on; release it otherwise. Safe from any thread.
static void cursor_update(HWND h) {
AcquireSRWLockExclusive(&cur_lock);
int want = h && cur_active && !(cur_rel & CUR_REL) && !cur_suspend && input_cursor_allowed() &&
GetForegroundWindow() == h && !IsIconic(h);
RECT r;
POINT tl = {0, 0};
if (want && GetClientRect(h, &r) && ClientToScreen(h, &tl)) {
RECT clip = {tl.x, tl.y, tl.x + r.right, tl.y + r.bottom};
ClipCursor(&clip);
if (!cur_captured) { InterlockedExchange(&mouse_dx, 0); InterlockedExchange(&mouse_dy, 0); }
InterlockedExchange(&cur_captured, 1);
} else {
if (cur_captured) ClipCursor(NULL);
InterlockedExchange(&cur_captured, 0);
}
ReleaseSRWLockExclusive(&cur_lock);
}
int input_cursor_released(void) { return (cur_rel & CUR_REL) != 0; }
void input_cursor_suspend(int on) {
InterlockedExchange(&cur_suspend, on != 0);
cursor_update(gfx_window_handle());
}
// Window procedure hook (gfx_window_win.c calls it first). Returns 1 with *res set when the message is fully handled.
int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
switch (m) {
case WM_CREATE: {
RAWINPUTDEVICE rid = {.usUsagePage = 1, .usUsage = 2, .dwFlags = 0, .hwndTarget = h}; // generic mouse
if (!RegisterRawInputDevices(&rid, 1, sizeof rid)) rt_log("input: raw mouse registration failed (%lu)", GetLastError());
break;
}
case WM_INPUT: {
RAWINPUT ri;
UINT sz = sizeof ri;
if (GetRawInputData((HRAWINPUT)l, RID_INPUT, &ri, &sz, sizeof(RAWINPUTHEADER)) != (UINT)-1 &&
ri.header.dwType == RIM_TYPEMOUSE && !(ri.data.mouse.usFlags & MOUSE_MOVE_ABSOLUTE)) {
LONG x = ri.data.mouse.lLastX, y = ri.data.mouse.lLastY;
InterlockedAdd(&mouse_travel, (x < 0 ? -x : x) + (y < 0 ? -y : y));
if (cur_captured) {
InterlockedAdd(&mouse_dx, x);
InterlockedAdd(&mouse_dy, y);
}
}
// The wheel comes in the same message (usButtonData, signed, + away from the player)
if (cur_captured && ri.header.dwType == RIM_TYPEMOUSE && (ri.data.mouse.usButtonFlags & RI_MOUSE_WHEEL))
InterlockedAdd(&mouse_wheel, (SHORT)ri.data.mouse.usButtonData);
break; // DefWindowProc must see WM_INPUT for cleanup
}
case WM_SETCURSOR:
if (cur_captured && LOWORD(l) == HTCLIENT) { SetCursor(NULL); *res = TRUE; return 1; }
break;
case WM_SYSCOMMAND: // Alt pressed alone would enter the window menu loop, which stalls the game: Alt is a game key
if ((w & 0xFFF0) == SC_KEYMENU && l == 0) { *res = 0; return 1; }
break;
case WM_KEYDOWN: // Esc is unbound in the game layout: it releases the cursor and opens the menu (menu_window_msg)
if (w == VK_ESCAPE && !(cur_rel & CUR_REL)) { cur_rel_update(rel_esc); cursor_update(h); }
break;
case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MBUTTONDOWN: // client area only; poll_merged drops the click
if (cur_rel & CUR_REL) { cur_rel_update(rel_clear); cursor_update(h); }
break;
case WM_MOUSEACTIVATE: // sent before WM_ACTIVATEAPP: a title bar or border click must not capture
if (LOWORD(l) != HTCLIENT) cur_rel_update(rel_title);
break;
case WM_ACTIVATEAPP:
if (!w) cur_rel_update(rel_clear); // Alt+Tab back in captures again
InterlockedExchange(&cur_active, w != 0);
cursor_update(h);
break;
case WM_SETFOCUS: case WM_KILLFOCUS: case WM_SIZE: case WM_MOVE:
cursor_update(h);
break;
case WM_ENTERSIZEMOVE:
input_cursor_suspend(1);
break;
case WM_EXITSIZEMOVE:
input_cursor_suspend(0);
break;
case WM_CLOSE:
InterlockedExchange(&cur_active, 0);
cursor_update(h);
break;
}
return 0;
}
static int key_down(int vk) { return (GetAsyncKeyState(vk) & 0x8000) != 0; }
int input_flight_key(void) {
HWND w = gfx_window_handle();
return w && w == GetForegroundWindow() && key_down(VK_F8);
}
// ---- input_wsys.h
int ikb_foreground(void) {
HWND w = gfx_window_handle();
return w && w == GetForegroundWindow();
}
int ikb_down(int key) {
int vk = key_vk(key);
return vk && key_down(vk);
}
int ikb_escape(void) { return key_down(VK_ESCAPE); }
int ikb_captured(void) { return cur_captured != 0; }
int ikb_pointer(float *nx, float *ny) {
HWND h = gfx_window_handle();
RECT r;
POINT p;
if (!h || GetForegroundWindow() != h || !GetClientRect(h, &r) || r.right < 1 || r.bottom < 1 || !GetCursorPos(&p) ||
!ScreenToClient(h, &p))
return 0;
*nx = (float)p.x / (float)r.right, *ny = (float)p.y / (float)r.bottom;
return p.x >= 0 && p.y >= 0 && p.x < r.right && p.y < r.bottom;
}
void ikb_pointer_set(float nx, float ny) {
HWND h = gfx_window_handle();
RECT r;
if (!h || GetForegroundWindow() != h || !GetClientRect(h, &r)) return;
POINT p = {(LONG)(nx * r.right), (LONG)(ny * r.bottom)};
if (ClientToScreen(h, &p)) SetCursorPos(p.x, p.y);
}
uint32_t ikb_mouse_travel(void) { return (uint32_t)mouse_travel; }
void ikb_mouse_take(int32_t *dx, int32_t *dy) {
*dx = InterlockedExchange(&mouse_dx, 0);
*dy = InterlockedExchange(&mouse_dy, 0);
}
int32_t ikb_wheel_take(void) {
LONG o, n;
do {
o = mouse_wheel;
n = o % WHEEL_DELTA; // the part of a notch stays for the next call
} while (InterlockedCompareExchange(&mouse_wheel, n, o) != o);
return (int32_t)(o / WHEEL_DELTA);
}
void ikb_cursor_update(void) { cursor_update(gfx_window_handle()); }
void ikb_menu_closed(void) { cur_rel_update(rel_menu_closed); }