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>
This commit is contained in:
Tempus3214andClaude Opus 5.5 committed 2026-11-05 03:48:13 -03:00
1 parent 5b4b300a1e
commit 21c65ec065
6 files changed
+70 -6

No files matched your search

+2 -2
View File
@@ -29,7 +29,7 @@ The log names the layout in effect (`input: gamepad layout ...`); the menu shows
| Minus | Back | M | map |
| Right stick shaken left and right | shake the right stick | G | detach a fused part from what Ultrahand holds |
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel is unbound. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
Esc is unbound in the game; it opens the settings menu and releases the mouse cursor (see Mouse camera). Z and X duplicate ZL and ZR so menu pages turn with two neighbouring keys; the menu tabs (L, R) are Q and R. The mouse wheel zooms the Purah Pad's map and is unbound elsewhere. The bindings are the tables of `input_map` in `runtime/input_map.c`: the keys (platform-neutral key ids) and one gamepad table per layout (gamepad inputs by position); `runtime/input_win.c` translates Win32 virtual keys and SDL buttons.
## Xbox button prompts
@@ -145,7 +145,7 @@ The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sti
## Mouse in the game's menus
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
On game 1.4.3, while one of the game's menus has its focus box (the frame the d-pad moves: the title, the save list, the inventory and its System tab, Options, shops, dialogue choices), the cursor is released from the mouse camera and shown in the middle of the window. A focus gap of up to 300 ms (a tab switch, a popup opening: the game drops the focus box for a frame or two) keeps the pointer where it is instead of capturing and centring it again. The item under the pointer takes the focus as if the d-pad had moved there, with the game's highlight, sound and description; a left click is A on it, a right click is B. The keys and the pad keep working, and the focus moves only when the pointer moves. This happens only while the keyboard and mouse are the device used last: after a gamepad button press or a stick pushed past half way the menus are as on the console, with no pointer, until the mouse moves or a key is pressed (`input_kbm_last` in `runtime/input.c`). The ability wheel, the d-pad strips, Autobuild and the Purah Pad's camera stay gameplay: the mouse keeps the camera there. The map has no focus box; it is found in `eui::ScreenMgr`'s list of open screens (every 100 ms: the Purah Pad's frame `AppMenuOverlay_00` with `AppMap_00` and the menu background `AppMenuBG_00`), and there the mouse moves the map's cursor as the left stick does, with the cursor still captured, and the scroll wheel zooms: each notch is a full-tilt push of the right stick (the map's zoom) for 100 ms with 50 ms of neutral after it, up (in) for a turn away from you; a fast scroll's notches wait their turn (at most four), a turn the other way drops them. The Scope opens the same map screen without the menu background and keeps the mouse camera. When the menu closes the cursor is captured again.
`runtime/menu_cursor.c` reads eui's screens at each pad read and moves the focus with the writes `eui::BoxCursorMgr::moveBoxCursor` makes, so no game code is called. `menu_cursor = 0` (`TOTK_MENU_CURSOR=0`, live; the *Mouse in the game's menus* row of the Controls tab) turns it off; `TOTK_MENU_CURSOR_LOG=1` logs every screen it reads (by default the first six), with the layout size, the node count and the hit panes it uses.
+35 -2
View File
@@ -541,6 +541,35 @@ static int32_t shake_stick(int held) {
return (now - from) / SHAKE_SIDE_NS % 2 ? STICK_MAX : -STICK_MAX;
}
// The scroll wheel on the Purah Pad's map, which zooms with the right stick's y (up: in): each notch is a full-tilt
// pulse of MAP_ZOOM_PULSE_NS on that stick with MAP_ZOOM_GAP_NS of neutral after it, so a step of the zoom and a
// held zoom both see one notch as one push. A fast scroll's notches wait their turn (at most MAP_ZOOM_QUEUE), a turn the
// other way drops them. Returns the stick y for this poll, 0 between pulses and off the map. Under lock.
#define MAP_ZOOM_PULSE_NS 100000000ull
#define MAP_ZOOM_GAP_NS 50000000ull
#define MAP_ZOOM_QUEUE 4
static int32_t map_zoom_stick(int32_t notches, int on_map) {
static int32_t queued, dir;
static uint64_t until; // the end of the pulse (dir != 0) or of the neutral after it
uint64_t now = plat_now_ns();
if (!on_map) { queued = dir = 0, until = 0; return 0; }
if (notches) {
if (queued && (notches > 0) != (queued > 0)) queued = 0;
queued += notches;
queued = queued > MAP_ZOOM_QUEUE ? MAP_ZOOM_QUEUE : queued < -MAP_ZOOM_QUEUE ? -MAP_ZOOM_QUEUE : queued;
}
if (dir) {
if (now < until) return dir * STICK_MAX;
dir = 0, until = now + MAP_ZOOM_GAP_NS;
return 0;
}
if (now < until || !queued) return 0;
dir = queued > 0 ? 1 : -1;
queued -= dir;
until = now + MAP_ZOOM_PULSE_NS;
return dir * STICK_MAX;
}
// Under lock, state 1: the open gamepad's inputs by position (pd[PAD_*] != 0 held); 0 without a connected gamepad.
static int pad_physical(uint8_t pd[PAD_COUNT]) {
if ((!pad || !SDL_GamepadConnected(pad)) && plat_now_ns() / 1000000u >= next_scan) { next_scan = plat_now_ns() / 1000000u + RESCAN_MS; scan_pads(); }
@@ -618,9 +647,13 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
int selector = b & (1ull << B_L) ? 1 : b & (1ull << B_UP) ? 2 :
b & (1ull << B_LEFT) ? 3 : b & (1ull << B_RIGHT) ? 4 : 0;
mouse_camera(k, selector);
// On the map the left stick moves the cursor and the right one zooms: the mouse moves the cursor there
if (menu_cursor_map()) pick(&sl[0], &sl[1], k[0], k[1]);
// On the map the left stick moves the cursor and the right one zooms: the mouse moves the cursor there,
// the scroll wheel zooms (map_zoom_stick). Elsewhere the wheel is unbound; its notches are dropped.
int map = menu_cursor_map();
if (map) pick(&sl[0], &sl[1], k[0], k[1]);
else pick(&sr[0], &sr[1], k[0], k[1]);
int32_t zoom = map_zoom_stick(ikb_wheel_take(), map);
if (zoom) pick(&sr[0], &sr[1], 0, zoom);
int32_t shake = shake_stick(input_map_held(down, B_SHAKE));
if (shake) sr[0] = shake, sr[1] = 0;
static uint32_t travel_seen;
+16
View File
@@ -21,6 +21,7 @@ static SDL_Window *win;
static volatile uint8_t key_held[KEY_COUNT];
static volatile int32_t esc_held, f8_held, focused;
static volatile int32_t mouse_dx, mouse_dy; // relative motion since the last ikb_mouse_take, whole counts
static volatile int32_t mouse_wheel; // wheel turned while captured, in 1/120 notches (ikb_wheel_take)
static float rem_x, rem_y; // window thread: the fractions SDL reported beyond them
static volatile int32_t captured; // relative mouse mode is on
enum { CUR_REL = 1, CUR_ESC = 2 }; // as input_win.c: released by Esc or a click elsewhere; by Esc
@@ -198,6 +199,12 @@ void input_sdl_event(const SDL_Event *e) {
if (dy) plat_add32(&mouse_dy, dy);
}
break;
case SDL_EVENT_MOUSE_WHEEL: // notches, + away from the player (a flipped wheel reports the other sign)
if (captured) {
float y = e->wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -e->wheel.y : e->wheel.y;
plat_add32(&mouse_wheel, (int32_t)(y * 120));
}
break;
case SDL_EVENT_WINDOW_FOCUS_GAINED:
plat_xchg32(&focused, 1);
break;
@@ -225,3 +232,12 @@ void ikb_mouse_take(int32_t *dx, int32_t *dy) {
*dx = plat_xchg32(&mouse_dx, 0);
*dy = plat_xchg32(&mouse_dy, 0);
}
int32_t ikb_wheel_take(void) {
int32_t o, n;
do {
o = mouse_wheel;
n = o % 120; // the part of a notch stays for the next call
} while (!__atomic_compare_exchange_n(&mouse_wheel, &o, n, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST));
return o / 120;
}
+13
View File
@@ -84,6 +84,7 @@ uint32_t input_key_char(int 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
@@ -147,6 +148,9 @@ int input_window_msg(HWND h, UINT m, WPARAM w, LPARAM l, LRESULT *res) {
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:
@@ -234,6 +238,15 @@ void ikb_mouse_take(int32_t *dx, int32_t *dy) {
*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); }
+2
View File
@@ -13,6 +13,8 @@ void ikb_pointer_set(float nx, float ny); // moves the pointer there (the wind
int ikb_captured(void); // the cursor is captured for the mouse camera
void ikb_mouse_take(int32_t *dx, int32_t *dy); // raw mouse motion since the last call, in counts
uint32_t ikb_mouse_travel(void); // raw counts the mouse moved in total (|dx| + |dy|), captured or not
int32_t ikb_wheel_take(void); // whole wheel notches turned while captured since the last call (+ away
// from the player); a high-resolution wheel's part of a notch is kept
void ikb_cursor_update(void); // what input_cursor_allowed says may have changed
void ikb_menu_closed(void); // the menu closed: a capture that Esc gave up comes back
+2 -2
View File
@@ -306,8 +306,8 @@
# mouse_mode = direct
# The mouse in the game's menus, 1.4.3 only (title, inventory, System tab, Options, shops, dialogue choices): while a
# menu has the game's focus box the pointer shows, the item under it takes the focus, a left click is A on it and a
# right click is B. On the map the mouse moves the map's cursor. 0 leaves the menus and the map to the keys and the
# pad. Changes while the game runs.
# right click is B. On the map the mouse moves the map's cursor and the scroll wheel zooms. 0 leaves the menus and
# the map to the keys and the pad. Changes while the game runs.
# menu_cursor = 1
# Mouse camera sensitivity (0 to 10; 0 = no mouse camera and no cursor capture).
# mouse_sens = 1