From 64e8b7a5fde958b29a0bc5ccc1482e072bfe503f Mon Sep 17 00:00:00 2001 From: hckrman101 <239247847+hckrman101@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:34:02 -0500 Subject: [PATCH] The Insert menu frees the mouse while it is open and takes its clicks: hover selects, tabs, value halves, toggles, sliders (click or drag), the wheel; the capture comes back on close --- gpu/vk/vk_api.h | 17 +++- gpu/vk/vk_menu.cpp | 203 +++++++++++++++++++++++++++++++++------ gpu/vk/vk_menu_hit.h | 122 +++++++++++++++++++++++ host/display.c | 73 +++++++++++++- host/hos_sdl.c | 44 +++++++-- host/hos_sdl.h | 7 +- host/posix/display_sdl.c | 66 ++++++++++++- host/rx_backend.c | 3 +- tests/run_all.py | 1 + tests/test_menu_hit.cpp | 133 +++++++++++++++++++++++++ tests/test_menu_hit.py | 62 ++++++++++++ 11 files changed, 685 insertions(+), 46 deletions(-) create mode 100644 gpu/vk/vk_menu_hit.h create mode 100644 tests/test_menu_hit.cpp create mode 100644 tests/test_menu_hit.py diff --git a/gpu/vk/vk_api.h b/gpu/vk/vk_api.h index 9a41f0b59..abb91274e 100644 --- a/gpu/vk/vk_api.h +++ b/gpu/vk/vk_api.h @@ -15,7 +15,7 @@ extern "C" { #endif -#define MKWVK_API_VERSION 4 +#define MKWVK_API_VERSION 5 /* (hookshot) the controls' names (host/nn_hid.c, host/pad_sdl.c, the pad_ and key_ lines of hookshot_options.ini): the Switch's buttons in nn::hid's bit order, and a gamepad's inputs by position, SDL's order (the bottom face @@ -30,6 +30,17 @@ enum { HOS_BIND_PAD, HOS_BIND_KEY }; enum { HOS_KEY_MOVE_UP = HOS_SW_N, HOS_KEY_MOVE_DOWN, HOS_KEY_MOVE_LEFT, HOS_KEY_MOVE_RIGHT, HOS_KEY_CAM_UP, HOS_KEY_CAM_DOWN, HOS_KEY_CAM_LEFT, HOS_KEY_CAM_RIGHT, HOS_KEY_N }; +/* (hookshot) the mouse over the window while it is free (not captured): the Insert menu's (host/display.c, + host/posix/display_sdl.c). x and y: the cursor in the window's pixels, of a window width x height pixels (the + swapchain's, as a rule); held: the buttons down now, pressed: those pressed since the last read (1 left, 2 right, + 4 middle); wheel: the wheel's notches since then, away from the player positive */ +enum { HOS_MOUSE_LEFT = 1, HOS_MOUSE_RIGHT = 2, HOS_MOUSE_MIDDLE = 4 }; +typedef struct MkwVkPointer { + int32_t x, y, width, height; + uint32_t held, pressed; + int32_t wheel; +} MkwVkPointer; + /* what the executable gives the back end */ typedef struct MkwVkHost { uint32_t version; /* MKWVK_API_VERSION */ @@ -82,6 +93,10 @@ typedef struct MkwVkHost { void (*bind)(int set, int button, int slot, int code); /// The guest camera aspect explicitly applied at startup (0: its default aspect) float (*aspect_ratio)(void); + /* (hookshot) the free mouse (MkwVkPointer), read and its presses and wheel cleared: 1 when the cursor is over the + window and the window has the focus. The Insert menu releases the mouse while it is open: WM_APP + 8 to the + window, wParam 1 open, 0 closed (the capture it had then comes back) */ + int (*pointer)(MkwVkPointer *out); } MkwVkHost; /* what the back end gives the executable: the rx_* functions (gpu/render/rx_nvn.h, rx_core) */ diff --git a/gpu/vk/vk_menu.cpp b/gpu/vk/vk_menu.cpp index 598f1f47b..5eb855e50 100644 --- a/gpu/vk/vk_menu.cpp +++ b/gpu/vk/vk_menu.cpp @@ -4,6 +4,10 @@ // (hookshot) A gamepad's Home button, or Select and Start together, opens it too; the D-pad chooses and changes, the bottom // face button changes too, the right one closes. The game gets no gamepad input while it is open either. // Its Controls page binds the Switch's buttons to gamepad inputs and keys. +// (hookshot) The mouse is free while it is open (the window lets it go: WM_APP + 8, host/display.c and +// host/posix/display_sdl.c): the row under the cursor is selected, a click on a tab shows it, on a value's left or right +// half changes it down or up, on a toggle flips it, on a slider's track sets it (or drag it); the wheel changes the value +// under the cursor, else moves the selection; a right click is Esc. vk_menu_hit.h has the layout. #include #include #include @@ -14,6 +18,7 @@ #include "vk_paths.h" #include "vk_menu.h" +#include "vk_menu_hit.h" #include "vk_work.h" // (after the others: counts the commands that do GPU work, for full_barrier) #ifndef HOOKSHOT_VERSION // (nx2/build.py gives it to this file: nx2/version.py) #define HOOKSHOT_VERSION "unknown" @@ -223,7 +228,7 @@ Row rows[] = { "Which gamepad button and which keys press each of the Switch's buttons."}, }; constexpr int kRows = sizeof rows / sizeof rows[0]; -constexpr int kMenuWidth = 480; // (at a scale of 1: 720 lines) +constexpr int kMenuWidth = menu_hit::kWidth; // (at a scale of 1: 720 lines) int cur_tab = TAB_GRAPHICS; int tab_selected[TAB_COUNT] = {0, 0, 0}; @@ -258,6 +263,15 @@ bool menu_refresh = false; int marquee_pos = 0, marquee_max = 0; int64_t marquee_next = 0; constexpr int64_t kMarqueeStep = 150000000LL, kMarqueeRest = 1200000000LL; // (ns) +// (hookshot) the mouse (menu_tick): the tab strip's part under the cursor (a tab 0 to 2, 3 the previous, 4 the next; -1 +// none); paint_menu's record of each shown row's value's left edge (as with its arrows, whether shown or not) and of the +// rows a slider's track was drawn on; the slider held with the left button (its row in the tab, -1 none) +int hover_tab = -1; +int row_value_l[kRows]; +bool row_slider[kRows]; +int drag_row = -1; +bool mouse_read = false; // (the menu has read the mouse since it opened) +bool is_slider(const Row& r) { return r.value == &opts.sharpness || r.value == &opts.mouse_sens; } std::string ini_path() { char exe[MAX_PATH]; const DWORD n = game_module_path(exe, MAX_PATH); @@ -335,6 +349,31 @@ void save() { } } +/// A row's new value v, as the arrows set it (dir: 1 up, -1 down, for the rows that skip or wrap), saved unless !keep +/// (a slider's drag: saved once it is let go) +void set_row(Row& r, int v, int dir, bool keep = true) { + if (r.value == &opts.framegen && v == 1) v = dir > 0 ? 2 : 0; // (off, 2x ... 8x) + if (r.value == &opts.mouse_sens) v = std::min(std::max(v, r.lo), r.hi); // (a scale: it stops at its ends) + if (r.value == &opts.pad_layout) { + if (v > 2) v = dir > 0 ? 0 : 2; + if (v < 0) v = 2; + *r.value = v; + apply_pad_layout(*r.value); + } else if (r.value == &opts.preset) { + *r.value = v > r.hi ? r.lo : v < r.lo ? r.hi : v; + apply_preset(*r.value); + } else { + *r.value = v > r.hi ? r.lo : v < r.lo ? r.hi : v; + if (r.value != &opts.fullscreen && r.value != &opts.show_fps && r.value != &opts.gpu_device && + r.value != &opts.invert_cam_x && r.value != &opts.invert_cam_y && r.value != &opts.mouse_native && + r.value != &opts.mouse_sens && r.value != &opts.mouse_invert) { // (the controls are not the preset's) + opts.preset = 0; // Custom + sync_preset_budgets(0); + } + } + if (keep) save(); +} + // ---- (hookshot) the Controls page: what presses each of the Switch's buttons, a gamepad input and up to two keys ---- struct Control { int button; // HOS_SW_*, or HOS_KEY_* (keys only) @@ -571,7 +610,8 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { SelectObject(dc, body); if (page == 1) { // (hookshot) the Controls page TextOutA(dc, 16 * s + 110 * s, 12 * s + 4 * s, "Controls", 8); - const int rh = 24 * s, top = 64 * s, xpad = 200 * s, xkey = 390 * s; + const int rh = menu_hit::kCtlRowH * s, top = menu_hit::kCtlTop * s, xpad = menu_hit::kCtlPad * s, + xkey = menu_hit::kCtlKey * s; SetTextColor(dc, RGB(140, 150, 144)); TextOutA(dc, 16 * s, 44 * s, "Button", 6); TextOutA(dc, xpad, 44 * s, "Gamepad", 7); @@ -595,7 +635,7 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { if (sel && col == ccol) { SIZE sz{}; GetTextExtentPoint32A(dc, v.c_str(), (int)v.size(), &sz); - fill(dc, (col ? xkey : xpad) - 6 * s, y + 2 * s, (int)sz.cx + 12 * s, rh - 4 * s, + fill(dc, (col ? xkey : xpad) - menu_hit::kCtlCellPad * s, y + 2 * s, (int)sz.cx + 12 * s, rh - 4 * s, listening ? RGB(150, 120, 40) : RGB(70, 120, 84)); } TextOutA(dc, col ? xkey : xpad, y + 3 * s, v.c_str(), (int)v.size()); @@ -613,23 +653,23 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { return; } // the tab strip: Graphics, Gameplay, Controls (Q and E, the bumpers or Tab change it: menu_tick) - const int tab_y = 44 * s; - const int tab_h = 32 * s; + const int tab_y = menu_hit::kTabY * s; + const int tab_h = menu_hit::kTabH * s; fill(dc, 0, tab_y, (int)w, tab_h, RGB(22, 28, 25)); HFONT small_font = font(12 * s, FW_SEMIBOLD); SelectObject(dc, small_font); - SetTextColor(dc, RGB(110, 135, 120)); + SetTextColor(dc, hover_tab == 3 ? RGB(210, 225, 215) : RGB(110, 135, 120)); // (lighter under the cursor: a click goes there) TextOutA(dc, 16 * s, tab_y + 8 * s, "[Q / LB]", 8); const char* rb_hint = "[E / RB]"; SIZE rbsz{}; GetTextExtentPoint32A(dc, rb_hint, (int)strlen(rb_hint), &rbsz); + SetTextColor(dc, hover_tab == 4 ? RGB(210, 225, 215) : RGB(110, 135, 120)); TextOutA(dc, (int)w - 16 * s - rbsz.cx, tab_y + 8 * s, rb_hint, (int)strlen(rb_hint)); HFONT tab_font = font(13 * s, FW_BOLD); SelectObject(dc, tab_font); const char* const tab_titles[TAB_COUNT] = {"GRAPHICS", "GAMEPLAY", "CONTROLS"}; - const int center_start = 85 * s; - const int center_width = (int)w - 170 * s; - const int tab_slot_w = center_width / TAB_COUNT; + const int center_start = menu_hit::kTabSide * s; + const int tab_slot_w = menu_hit::tab_slot_w(s, (int)w); for (int t = 0; t < TAB_COUNT; t++) { const bool active = (t == cur_tab); int slot_x = center_start + t * tab_slot_w; @@ -641,6 +681,9 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { fill(dc, text_x - 10 * s, tab_y + 3 * s, tsz.cx + 20 * s, tab_h - 6 * s, RGB(38, 64, 48)); fill(dc, text_x - 10 * s, tab_y + tab_h - 2 * s, tsz.cx + 20 * s, 2 * s, RGB(235, 205, 95)); SetTextColor(dc, RGB(255, 255, 255)); + } else if (t == hover_tab) { // (under the cursor: a dimmer pill) + fill(dc, text_x - 10 * s, tab_y + 3 * s, tsz.cx + 20 * s, tab_h - 6 * s, RGB(30, 46, 37)); + SetTextColor(dc, RGB(215, 225, 218)); } else { SetTextColor(dc, RGB(135, 150, 142)); } @@ -651,7 +694,7 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { DeleteObject(small_font); fill(dc, 0, tab_y + tab_h, (int)w, std::max(1, s), RGB(52, 68, 58)); - const int rh = 28 * s, top = tab_y + tab_h + 8 * s; + const int rh = menu_hit::kRowH * s, top = menu_hit::rows_top(s); const int tab_count = tab_row_count(cur_tab); marquee_max = 0; // (set again below while the selected row's value is too long) for (int i = 0; i < tab_count; i++) { @@ -696,6 +739,18 @@ void paint_menu(HDC dc, uint32_t w, uint32_t h, int s) { marquee_max = 0; } TextOutA(dc, (int)w - 18 * s - width(v), y + 4 * s, v.c_str(), (int)v.size()); + // (the mouse's: where the value is, its arrows counted whether they show or not, so a click's half does not move + // as the row is selected) + row_value_l[i] = (int)w - 18 * s - std::min(r.value ? width("< " + label + " >") : width(v), room); + // (a slider's track between the name and the value, with a knob at the value: click or drag it) + const int tl = menu_hit::kSliderL * s, tr = menu_hit::kSliderR * s; + row_slider[i] = is_slider(r) && 18 * s + (int)nsz.cx + 8 * s <= tl && row_value_l[i] >= tr + 8 * s; + if (row_slider[i]) { + const int ty = y + rh / 2 - s, knob = tl + (tr - tl) * menu_hit::slider_permille(*r.value, r.lo, r.hi) / 1000; + fill(dc, tl, ty, tr - tl, 2 * s, RGB(60, 80, 68)); + fill(dc, tl, ty, knob - tl, 2 * s, RGB(96, 210, 120)); + fill(dc, knob - 3 * s, y + rh / 2 - 6 * s, 6 * s, 12 * s, sel ? RGB(255, 255, 255) : RGB(176, 186, 180)); + } } const int help_top = top + max_tab_rows() * rh + 8 * s; // (the same height whichever tab is shown) fill(dc, 16 * s, help_top - 4 * s, (int)w - 32 * s, std::max(1, s), RGB(52, 64, 56)); @@ -971,6 +1026,27 @@ void menu_tick() { bool changed = scale != cur_scale || menu_refresh; menu_refresh = false; cur_scale = scale; + // (hookshot) the mouse, while the menu is open: the cursor in the menu's pixels (mx, my), whether it moved since the + // last frame, and its clicks and wheel since then (vk_menu_hit.h) + MkwVkPointer ptr{}; + int mx = -1, my = -1, mw = 0, mh = 0; + bool mouse = false, moved = false; + if (open_now.load() && host && host->pointer) { + mouse = host->pointer(&ptr) != 0; + { + std::lock_guard lk(bmp_mu); + mw = (int)menu_bmp.w, mh = (int)menu_bmp.h; + } + int ix = 0, iy = 0, x0 = 0, y0 = 0; + menu_hit::to_image(ptr.x, ptr.y, ptr.width, ptr.height, (int)dev.swap_extent.width, (int)dev.swap_extent.height, + &ix, &iy); + menu_hit::origin((int)dev.swap_extent.width, (int)dev.swap_extent.height, mw, mh, &x0, &y0); + if (mouse) mx = ix - x0, my = iy - y0; + static int last_x = -1, last_y = -1; + moved = mouse && mouse_read && (mx != last_x || my != last_y); // (not the first read: a cursor resting over the + last_x = mx, last_y = my, mouse_read = true; // menu as it opens leaves the selection alone) + if (!mouse) ptr.pressed = 0, ptr.wheel = 0; + } if (press[0] || press[6]) { open_now = !open_now.load(), changed = true; page = 0, listening = 0; @@ -1008,6 +1084,21 @@ void menu_tick() { } } } else { + if (mouse) { // (the mouse: the row under it selected, a click on a binding listens for its new input, on + // Reset resets; the wheel moves the selection, a right click goes back) + const menu_hit::Hit hit = menu_hit::controls(mx, my, scale, mw, mh, kControls); + const bool on = hit.part == menu_hit::kCtlRow || hit.part == menu_hit::kCtlCell; + if (on && (moved || ptr.pressed)) { + if (hit.index != csel) csel = hit.index, changed = true; + if (hit.part == menu_hit::kCtlCell && !keys_only(csel) && ccol != hit.side) ccol = hit.side, changed = true; + } + if (ptr.pressed & HOS_MOUSE_LEFT) { + if (hit.part == menu_hit::kCtlCell || (hit.part == menu_hit::kCtlRow && hit.index == kControls)) + press[7] = true; // (as Enter) + } + if (ptr.pressed & HOS_MOUSE_RIGHT) press[5] = true; + if (ptr.wheel) csel = ((csel - ptr.wheel) % (kControls + 1) + kControls + 1) % (kControls + 1), changed = true; + } if (press[5]) page = 0, changed = true; if (press[1]) csel = (csel + kControls) % (kControls + 1), changed = true; if (press[2]) csel = (csel + 1) % (kControls + 1), changed = true; @@ -1034,6 +1125,64 @@ void menu_tick() { } if (changed) redraw_menu(scale); } else if (open_now.load()) { + if (mouse) { // (the mouse: see the top of this file) + const int count = tab_row_count(cur_tab); + const menu_hit::Hit hit = menu_hit::options(mx, my, scale, mw, mh, TAB_COUNT, count, row_value_l, row_slider); + const int ht = hit.part == menu_hit::kTab ? hit.index : hit.part == menu_hit::kTabPrev ? 3 + : hit.part == menu_hit::kTabNext ? 4 : -1; + if (ht != hover_tab) hover_tab = ht, changed = true; + int& sel = tab_selected[cur_tab]; + const bool on_row = hit.part == menu_hit::kRow || hit.part == menu_hit::kSlider; + if (drag_row >= 0) { // (a slider held: it follows the cursor's x until let go) + Row& d = rows[tab_row_to_global(cur_tab, drag_row)]; + if (!(ptr.held & HOS_MOUSE_LEFT) || !d.value) { + drag_row = -1; + save(); + } else { + const int v = menu_hit::slider_value(menu_hit::slider_drag(mx, scale), d.lo, d.hi, d.step); + if (v != *d.value) set_row(d, v, v > *d.value ? 1 : -1, false), changed = true; + } + } else if (on_row && (moved || ptr.pressed) && hit.index != sel) { + sel = hit.index, changed = true; + } + if ((ptr.pressed & HOS_MOUSE_LEFT) && drag_row < 0) { + if (hit.part == menu_hit::kTabPrev) tab_left = true; + else if (hit.part == menu_hit::kTabNext) tab_right = true; + else if (hit.part == menu_hit::kTab && hit.index != cur_tab) cur_tab = hit.index, changed = true; + else if (hit.part == menu_hit::kSlider) { + Row& d = rows[tab_row_to_global(cur_tab, hit.index)]; + drag_row = hit.index; + const int v = menu_hit::slider_value(hit.permille, d.lo, d.hi, d.step); + if (v != *d.value) set_row(d, v, v > *d.value ? 1 : -1, false); + changed = true; + } else if (hit.part == menu_hit::kRow) { + Row& d = rows[tab_row_to_global(cur_tab, hit.index)]; + if (!d.value) press[7] = true; // (a page: as Enter) + else if (d.hi - d.lo == d.step || hit.side >= 0) press[4] = true; // (a toggle flips; the right half, or the name, as Right) + else press[3] = true; // (the left half as Left) + } + } + if (ptr.pressed & HOS_MOUSE_RIGHT) press[5] = true; // (as Esc) + if (ptr.wheel && drag_row < 0) { + Row& d = rows[tab_row_to_global(cur_tab, sel)]; + const bool on_value = on_row && hit.index == sel && d.value && + (hit.part == menu_hit::kSlider || hit.side != 0); + if (hit.part == menu_hit::kTab || hit.part == menu_hit::kTabPrev || hit.part == menu_hit::kTabNext) { + if (ptr.wheel > 0) tab_left = true; + else tab_right = true; + } else if (on_value) { // (over a value: it changes, up for the wheel away; a slider stops at its ends) + const int dir = ptr.wheel > 0 ? 1 : -1; + for (int n = ptr.wheel > 0 ? ptr.wheel : -ptr.wheel; n > 0; n--) { + const int v = *d.value + dir * d.step; + if (is_slider(d) && (v < d.lo || v > d.hi)) break; + set_row(d, v, dir); + } + changed = true; + } else if (on_row || hit.part == menu_hit::kNone) { // (elsewhere on the menu: the selection moves) + sel = ((sel - ptr.wheel) % count + count) % count, changed = true; + } + } + } if (press[5]) open_now = false; // (the tabs: LB and RB, Q and E, or Tab) if (tab_left) cur_tab = (cur_tab + TAB_COUNT - 1) % TAB_COUNT, changed = true; @@ -1046,28 +1195,8 @@ void menu_tick() { if (press[7] && !r.value && can_bind()) page = 1, csel = 0, ccol = 0, changed = true; if ((press[3] || press[4] || press[7]) && r.value) { // (Enter, or the bottom face button, as Right) if (press[7]) press[4] = true; - int v = *r.value + (press[4] ? r.step : -r.step); - if (r.value == &opts.framegen && v == 1) v = press[4] ? 2 : 0; // (off, 2x ... 8x) - if (r.value == &opts.mouse_sens) v = std::min(std::max(v, r.lo), r.hi); // (a scale: it stops at its ends) - if (r.value == &opts.pad_layout) { - if (v > 2) v = press[4] ? 0 : 2; - if (v < 0) v = 2; - *r.value = v; - apply_pad_layout(*r.value); - } else if (r.value == &opts.preset) { - *r.value = v > r.hi ? r.lo : v < r.lo ? r.hi : v; - apply_preset(*r.value); - } else { - *r.value = v > r.hi ? r.lo : v < r.lo ? r.hi : v; - if (r.value != &opts.fullscreen && r.value != &opts.show_fps && r.value != &opts.gpu_device && - r.value != &opts.invert_cam_x && r.value != &opts.invert_cam_y && r.value != &opts.mouse_native && - r.value != &opts.mouse_sens && r.value != &opts.mouse_invert) { // (the controls are not the preset's) - opts.preset = 0; // Custom - sync_preset_budgets(0); - } - } + set_row(r, *r.value + (press[4] ? r.step : -r.step), press[4] ? 1 : -1); changed = true; - save(); } const int64_t now = prof_now(); if (changed) { // (a new row or value: its name from the start, after a rest) @@ -1080,6 +1209,18 @@ void menu_tick() { } if (changed) redraw_menu(scale); } + { // (hookshot) the window lets the mouse go while the menu is open, and takes it back after (host/display.c) + static bool told = false; + const bool now_open = open_now.load(); + if (!now_open) { + if (drag_row >= 0) save(); // (a slider let go off the window: kept now) + hover_tab = -1, drag_row = -1, mouse_read = false; + } + if (now_open != told && app_window()) { + told = now_open; + PostMessageA(app_window(), WM_APP + 8, (WPARAM)now_open, 0); + } + } if (opts.show_fps) { static int64_t next = 0, last_game = 0, last_shown = 0; const int64_t now = prof_now(); diff --git a/gpu/vk/vk_menu_hit.h b/gpu/vk/vk_menu_hit.h new file mode 100644 index 000000000..22574e7d4 --- /dev/null +++ b/gpu/vk/vk_menu_hit.h @@ -0,0 +1,122 @@ +// The Insert menu's layout (vk_menu.cpp draws with these sizes, times its scale) and what a point over it is on: the +// mouse's hover and clicks while the menu is open. Plain arithmetic, no Vulkan or GDI: tests/test_menu_hit.cpp checks it. +#pragma once + +namespace mkwvk::menu_hit { + +// ---- the options page, at a scale of 1 (the window 720 pixels high; 1440 and up: 2, and so on) ---- +constexpr int kWidth = 480; +constexpr int kTabY = 44, kTabH = 32; // the tab strip +constexpr int kTabSide = 85; // either side of the tab titles: [Q / LB] and [E / RB], the previous and next tab +constexpr int kRowGap = 8, kRowH = 28; // the rows, from kTabY + kTabH + kRowGap +constexpr int kRowX = 8; // a row's band, kRowX in from either side +constexpr int kValueRight = 18; // a value ends kValueRight from the right +constexpr int kSliderL = 200, kSliderR = 330; // a slider row's track (Sharpness, Mouse sensitivity) +constexpr int kSliderGrab = 6; // (a click this far past either end still takes the track's end) +// ---- the Controls page ---- +constexpr int kCtlWidth = 600, kCtlTop = 64, kCtlRowH = 24, kCtlPad = 200, kCtlKey = 390, kCtlCellPad = 6; + +enum Part { + kNone, // off the menu's controls (still over the menu: a click does nothing) + kOutside, // off the menu + kTabPrev, // the tab strip's left end + kTab, // a tab: index + kTabNext, // its right end + kRow, // an option's row: index (the tab's own), side -1 the value's left half, 1 its right half, 0 the name + kSlider, // a slider row's track: index, permille 0 to 1000 along it + kCtlRow, // the Controls page: a row's name (index; index == the count: Reset to defaults) + kCtlCell, // a binding: index the row, side 0 the gamepad's, 1 the keyboard's +}; +struct Hit { + Part part = kNone; + int index = -1; + int side = 0; + int permille = 0; +}; + +/// A point in the window's pixels (the cursor's, in a window win_w x win_h pixels) in the window's image (img_w x img_h: +/// the swapchain's; the same size as a rule, but not while a resize is on its way) +inline void to_image(int x, int y, int win_w, int win_h, int img_w, int img_h, int* ix, int* iy) { + *ix = win_w > 0 ? (int)((long long)x * img_w / win_w) : x; + *iy = win_h > 0 ? (int)((long long)y * img_h / win_h) : y; +} +/// The menu's top left in the window's image: centred, as menu_record copies it +inline void origin(int img_w, int img_h, int menu_w, int menu_h, int* x0, int* y0) { + *x0 = (img_w - menu_w) / 2; + *y0 = (img_h - menu_h) / 2; +} +inline int rows_top(int s) { return (kTabY + kTabH + kRowGap) * s; } +inline int tab_slot_w(int s, int w) { return (w - 2 * kTabSide * s) / 3; } +/// A slider's value for a point along its track (lo to hi in steps of step), nearest step, held to its ends +inline int slider_value(int permille, int lo, int hi, int step) { + if (step <= 0 || hi <= lo) return lo; + const int steps = (hi - lo) / step; + const int k = (permille * steps + 500) / 1000; + return lo + (k < 0 ? 0 : k > steps ? steps : k) * step; +} +/// Where along the track a value is, in permille (the knob's place) +inline int slider_permille(int v, int lo, int hi) { + if (hi <= lo) return 0; + const int p = (int)((long long)(v - lo) * 1000 / (hi - lo)); + return p < 0 ? 0 : p > 1000 ? 1000 : p; +} + +/// The options page: (x, y) in the menu's own pixels (0, 0 its top left), w x h its size, s its scale; rows the shown +/// tab's row count; value_l[i] the left edge of row i's value as last drawn; slider[i] whether row i has a track. +/// A drag (dragging) keeps to the track whatever the point's height: slider_drag +inline Hit options(int x, int y, int s, int w, int h, int tabs, int rows, const int* value_l, const bool* slider) { + Hit hit; + if (x < 0 || y < 0 || x >= w || y >= h) return hit.part = kOutside, hit; + if (y >= kTabY * s && y < (kTabY + kTabH) * s) { + const int side = kTabSide * s, slot = tab_slot_w(s, w); + if (x < side) hit.part = kTabPrev; + else if (x >= w - side) hit.part = kTabNext; + else if (slot > 0) { + hit.part = kTab; + hit.index = (x - side) / slot; + if (hit.index >= tabs) hit.index = tabs - 1; + } + return hit; + } + const int top = rows_top(s); + if (y < top || x < kRowX * s || x >= w - kRowX * s) return hit; + const int i = (y - top) / (kRowH * s); + if (i >= rows) return hit; + hit.index = i; + if (slider && slider[i] && x >= (kSliderL - kSliderGrab) * s && x < (kSliderR + kSliderGrab) * s) { + hit.part = kSlider; + const int p = (int)((long long)(x - kSliderL * s) * 1000 / ((kSliderR - kSliderL) * s)); + hit.permille = p < 0 ? 0 : p > 1000 ? 1000 : p; + return hit; + } + hit.part = kRow; + const int vl = value_l ? value_l[i] : w; + const int vr = w - kValueRight * s; + if (x >= vl && vl < vr) hit.side = x < (vl + vr) / 2 ? -1 : 1; + return hit; +} +/// A drag that started on a slider: the point's place along the track, held to its ends, wherever the cursor went +inline int slider_drag(int x, int s) { + const int p = (int)((long long)(x - kSliderL * s) * 1000 / ((kSliderR - kSliderL) * s)); + return p < 0 ? 0 : p > 1000 ? 1000 : p; +} + +/// The Controls page: (x, y) in its pixels, w x h its size, s its scale; count the controls (then Reset to defaults) +inline Hit controls(int x, int y, int s, int w, int h, int count) { + Hit hit; + if (x < 0 || y < 0 || x >= w || y >= h) return hit.part = kOutside, hit; + const int top = kCtlTop * s; + if (y < top || x < kRowX * s || x >= w - kRowX * s) return hit; + const int i = (y - top) / (kCtlRowH * s); + if (i > count) return hit; + hit.index = i; + if (i < count && x >= (kCtlPad - kCtlCellPad) * s) { + hit.part = kCtlCell; + hit.side = x >= (kCtlKey - kCtlCellPad) * s ? 1 : 0; + } else { + hit.part = kCtlRow; + } + return hit; +} + +} // namespace mkwvk::menu_hit diff --git a/host/display.c b/host/display.c index 74951fd5f..0065d465a 100644 --- a/host/display.c +++ b/host/display.c @@ -6,7 +6,8 @@ HOS_WINDOW_NOACTIVATE=1: the window opens behind the one in front (test runs do not take the keyboard). HOS_WINDOW_SIZE=x: the client area (default 1280x720 at 96 dpi). F11 or Alt+Enter: fullscreen. Mouse: a click in the window captures the mouse (hidden, kept in the window), Esc or leaving the window releases - it (nn_hid.c reads its motion). */ + it (nn_hid.c reads its motion). While the Insert menu is open the mouse is free (the cursor shown, for the menu's + clicks: hos_mouse_pointer) and the game gets none of it; the capture it had comes back when the menu closes. */ #include "hos.h" #include #include @@ -14,6 +15,7 @@ #define WIN32_LEAN_AND_MEAN #include #include "../gpu/render/rx.h" +#include "../gpu/vk/vk_api.h" #ifndef WM_DPICHANGED #define WM_DPICHANGED 0x02E0 #endif @@ -39,6 +41,24 @@ void hos_mouse_give_back(int32_t dx, int32_t dy) { InterlockedExchangeAdd(&mouse_dy, dy); } +/* the free mouse, for the Insert menu (gpu/vk/vk_api.h MkwVkPointer): set on the window's thread, read on the renderer's */ +static volatile LONG ptr_x, ptr_y, ptr_w, ptr_h, ptr_in, ptr_pressed, ptr_wheel; /* (client pixels; wheel in WHEEL_DELTA units) */ +static int menu_open, captured_before_menu; + +int hos_mouse_pointer(MkwVkPointer *out) { + memset(out, 0, sizeof *out); + if (!hwnd || mouse_captured) return 0; + out->x = ptr_x, out->y = ptr_y, out->width = ptr_w, out->height = ptr_h; + out->held = ((GetAsyncKeyState(VK_LBUTTON) & 0x8000) ? HOS_MOUSE_LEFT : 0) | + ((GetAsyncKeyState(VK_RBUTTON) & 0x8000) ? HOS_MOUSE_RIGHT : 0) | + ((GetAsyncKeyState(VK_MBUTTON) & 0x8000) ? HOS_MOUSE_MIDDLE : 0); + out->pressed = (uint32_t)InterlockedExchange(&ptr_pressed, 0); + const LONG wheel = InterlockedExchange(&ptr_wheel, 0); + out->wheel = wheel / WHEEL_DELTA; + if (wheel % WHEEL_DELTA) InterlockedExchangeAdd(&ptr_wheel, wheel % WHEEL_DELTA); /* (a part of a notch: kept) */ + return ptr_in && GetForegroundWindow() == hwnd && out->width > 0 && out->height > 0; +} + static HANDLE renderer_up; static int fullscreen; static WINDOWPLACEMENT windowed_place; @@ -98,8 +118,55 @@ static void mouse_capture(HWND w, int on) { } } +/* the menu opened: the mouse let go (its capture kept for later); closed: the capture back, if the window still has the focus */ +static void menu_mouse(HWND w, int open) { + if (open == menu_open) return; + menu_open = open; + if (open) { + captured_before_menu = (int)mouse_captured; + mouse_capture(w, 0); + InterlockedExchange(&ptr_pressed, 0), InterlockedExchange(&ptr_wheel, 0); + } else { + if (captured_before_menu && GetForegroundWindow() == w) mouse_capture(w, 1); + captured_before_menu = 0; + } +} +static void menu_press(HWND w, LPARAM lp, LONG bit) { /* (a click while the menu is open: the menu's) */ + ptr_x = (short)LOWORD(lp), ptr_y = (short)HIWORD(lp), ptr_in = 1; + InterlockedOr(&ptr_pressed, bit); + SetCapture(w); /* (a drag past the window's edge still reaches it) */ +} + static LRESULT CALLBACK wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { + case WM_MOUSEMOVE: + ptr_x = (short)LOWORD(lp), ptr_y = (short)HIWORD(lp); + if (!ptr_in) { + TRACKMOUSEEVENT t = {sizeof t, TME_LEAVE, w, 0}; + TrackMouseEvent(&t); + ptr_in = 1; + } + return 0; + case WM_MOUSELEAVE: + ptr_in = 0; + return 0; + case WM_MOUSEWHEEL: + if (menu_open) InterlockedExchangeAdd(&ptr_wheel, GET_WHEEL_DELTA_WPARAM(wp)); + return 0; + case WM_RBUTTONDOWN: + if (menu_open) menu_press(w, lp, HOS_MOUSE_RIGHT); + return 0; + case WM_MBUTTONDOWN: + if (menu_open) menu_press(w, lp, HOS_MOUSE_MIDDLE); + return 0; + case WM_LBUTTONUP: + case WM_RBUTTONUP: + case WM_MBUTTONUP: + if (GetCapture() == w && !(wp & (MK_LBUTTON | MK_RBUTTON | MK_MBUTTON))) ReleaseCapture(); + return 0; + case WM_APP + 8: /* (the Insert menu opened or closed: gpu/vk/vk_menu.cpp) */ + menu_mouse(w, (int)wp); + return 0; case WM_TIMER: return 0; case WM_INPUT: { @@ -113,7 +180,8 @@ static LRESULT CALLBACK wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { return DefWindowProcA(w, msg, wp, lp); } case WM_LBUTTONDOWN: - if (renderer && !mouse_captured) mouse_capture(w, 1); + if (menu_open) menu_press(w, lp, HOS_MOUSE_LEFT); + else if (renderer && !mouse_captured) mouse_capture(w, 1); return 0; case WM_KEYDOWN: if (wp == VK_ESCAPE) mouse_capture(w, 0); @@ -152,6 +220,7 @@ static LRESULT CALLBACK wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) { if (mouse_captured) { mouse_capture(w, 0); mouse_capture(w, 1); } /* (the clip follows the window) */ break; case WM_SIZE: + ptr_w = LOWORD(lp), ptr_h = HIWORD(lp); if (mouse_captured) { mouse_capture(w, 0); mouse_capture(w, 1); } if (renderer) rx_resize(LOWORD(lp), HIWORD(lp)); return 0; diff --git a/host/hos_sdl.c b/host/hos_sdl.c index cb8fcc577..964718976 100644 --- a/host/hos_sdl.c +++ b/host/hos_sdl.c @@ -15,7 +15,7 @@ static struct { /* window */ void *CreateWindow, *SetWindowPosition, *SetWindowFullscreen, *SetRelativeMouseMode, *GetPrimaryDisplay, *GetDesktopDisplayMode, *Vulkan_CreateSurface, *PixelSize, *GetKeyboardState, *GetMouseState, *WaitEventTimeout, - *PollEvent, *LoadPNG, *SetWindowIcon, *DestroySurface; + *PollEvent, *GetWindowSize, *LoadPNG, *SetWindowIcon, *DestroySurface; /* sound */ void *OpenAudioDevice, *PauseAudioDevice, *OpenAudioDeviceStream, *ResumeAudioStreamDevice, *PutAudioStreamData; /* controllers */ @@ -47,6 +47,7 @@ static int get_all(HMODULE m, int three) { GET2(PixelSize, "SDL_GetWindowSizeInPixels", "SDL_Vulkan_GetDrawableSize"); GET(GetKeyboardState, "SDL_GetKeyboardState"); GET(GetMouseState, "SDL_GetMouseState"); + GET(GetWindowSize, "SDL_GetWindowSize"); GET(WaitEventTimeout, "SDL_WaitEventTimeout"); GET(PollEvent, "SDL_PollEvent"); GET2(PadOpen, "SDL_OpenGamepad", "SDL_GameControllerOpen"); @@ -183,6 +184,10 @@ uint32_t hos_sdl_mouse_buttons(void) { return version == 3 ? CALL(uint32_t (*)(float *, float *), GetMouseState)(NULL, NULL) : CALL(uint32_t (*)(int *, int *), GetMouseState)(NULL, NULL); } +void hos_sdl_window_size(void *window, int *width, int *height) { + if (version == 3) CALL(_Bool (*)(void *, int *, int *), GetWindowSize)(window, width, height); + else CALL(void (*)(void *, int *, int *), GetWindowSize)(window, width, height); +} /* ---- events ---- */ static void read_event(const uint8_t *raw, HosSdlEvent *e) { @@ -203,18 +208,32 @@ static void read_event(const uint8_t *raw, HosSdlEvent *e) { memcpy(i, raw + 24, 4), e->scancode = i[0]; memcpy(&e->mod, raw + 32, 2), e->repeat = raw[37]; break; - case 0x400: /* (SDL_MouseMotionEvent: xrel and yrel at 36 and 40, floats) */ + case 0x20c: e->type = HOS_SDL_EV_MOUSE_ENTER; break; + case 0x400: /* (SDL_MouseMotionEvent: x and y at 28 and 32, xrel and yrel at 36 and 40, floats) */ e->type = HOS_SDL_EV_MOUSE_MOTION; + memcpy(&e->x, raw + 28, 4), memcpy(&e->y, raw + 32, 4); memcpy(&e->dx, raw + 36, 4), memcpy(&e->dy, raw + 40, 4); break; - case 0x401: e->type = HOS_SDL_EV_MOUSE_DOWN; break; + case 0x401: + case 0x402: /* (SDL_MouseButtonEvent: button at 24, x and y at 28 and 32, floats) */ + e->type = type == 0x401 ? HOS_SDL_EV_MOUSE_DOWN : HOS_SDL_EV_MOUSE_UP; + e->button = raw[24]; + memcpy(&e->x, raw + 28, 4), memcpy(&e->y, raw + 32, 4); + break; + case 0x403: { /* (SDL_MouseWheelEvent: y at 28, a float; direction at 32, 1 flipped) */ + uint32_t dir; + e->type = HOS_SDL_EV_MOUSE_WHEEL; + memcpy(&e->wheel, raw + 28, 4), memcpy(&dir, raw + 32, 4); + if (dir == 1) e->wheel = -e->wheel; + break; + } } return; } switch (type) { case 0x100: e->type = HOS_SDL_EV_QUIT; break; case 0x200: /* (SDL_WindowEvent: what at 12) */ - e->type = raw[12] == 6 ? HOS_SDL_EV_RESIZED : raw[12] == 11 ? HOS_SDL_EV_MOUSE_LEAVE + e->type = raw[12] == 6 ? HOS_SDL_EV_RESIZED : raw[12] == 10 ? HOS_SDL_EV_MOUSE_ENTER : raw[12] == 11 ? HOS_SDL_EV_MOUSE_LEAVE : raw[12] == 12 ? HOS_SDL_EV_FOCUS_GAINED : raw[12] == 13 ? HOS_SDL_EV_FOCUS_LOST : raw[12] == 14 ? HOS_SDL_EV_CLOSE : HOS_SDL_EV_OTHER; break; @@ -223,11 +242,24 @@ static void read_event(const uint8_t *raw, HosSdlEvent *e) { memcpy(i, raw + 16, 4), e->scancode = i[0]; memcpy(&e->mod, raw + 24, 2), e->repeat = raw[13]; break; - case 0x400: /* (SDL_MouseMotionEvent: xrel and yrel at 28 and 32, whole pixels) */ + case 0x400: /* (SDL_MouseMotionEvent: x and y at 20 and 24, xrel and yrel at 28 and 32, whole pixels) */ e->type = HOS_SDL_EV_MOUSE_MOTION; + memcpy(i, raw + 20, 8), e->x = (float)i[0], e->y = (float)i[1]; memcpy(i, raw + 28, 8), e->dx = (float)i[0], e->dy = (float)i[1]; break; - case 0x401: e->type = HOS_SDL_EV_MOUSE_DOWN; break; + case 0x401: + case 0x402: /* (SDL_MouseButtonEvent: button at 16, x and y at 20 and 24, whole pixels) */ + e->type = type == 0x401 ? HOS_SDL_EV_MOUSE_DOWN : HOS_SDL_EV_MOUSE_UP; + e->button = raw[16]; + memcpy(i, raw + 20, 8), e->x = (float)i[0], e->y = (float)i[1]; + break; + case 0x403: { /* (SDL_MouseWheelEvent: y at 20, whole notches; direction at 24, 1 flipped) */ + uint32_t dir; + e->type = HOS_SDL_EV_MOUSE_WHEEL; + memcpy(i, raw + 20, 4), memcpy(&dir, raw + 24, 4); + e->wheel = (float)(dir == 1 ? -i[0] : i[0]); + break; + } } } int hos_sdl_wait_event(HosSdlEvent *e, int timeout_ms) { diff --git a/host/hos_sdl.h b/host/hos_sdl.h index f2f882886..20e82e02e 100644 --- a/host/hos_sdl.h +++ b/host/hos_sdl.h @@ -25,18 +25,23 @@ int hos_sdl_vulkan_surface(void *window, void *instance, void *surface_out); / void hos_sdl_pixel_size(void *window, int *width, int *height); const uint8_t *hos_sdl_keys(int *count); /* the keyboard by scancode (USB HID usage), 1 a key down */ uint32_t hos_sdl_mouse_buttons(void); /* 1 left, 2 middle, 4 right */ +void hos_sdl_window_size(void *window, int *width, int *height); /* (in the units of the mouse's position, not pixels) */ void hos_sdl_icon(void *window, const char *png); /* (SDL 3.4's PNG loader: nothing on SDL2 or an older SDL3) */ /* ---- events, as the window's loop wants them ---- */ enum { HOS_SDL_EV_OTHER, HOS_SDL_EV_QUIT, HOS_SDL_EV_CLOSE, HOS_SDL_EV_RESIZED, HOS_SDL_EV_FOCUS_GAINED, - HOS_SDL_EV_FOCUS_LOST, HOS_SDL_EV_MOUSE_LEAVE, HOS_SDL_EV_KEY_DOWN, HOS_SDL_EV_MOUSE_DOWN, HOS_SDL_EV_MOUSE_MOTION + HOS_SDL_EV_FOCUS_LOST, HOS_SDL_EV_MOUSE_LEAVE, HOS_SDL_EV_KEY_DOWN, HOS_SDL_EV_MOUSE_DOWN, HOS_SDL_EV_MOUSE_MOTION, + HOS_SDL_EV_MOUSE_UP, HOS_SDL_EV_MOUSE_WHEEL, HOS_SDL_EV_MOUSE_ENTER }; typedef struct { int type; int scancode, repeat; /* (key down) */ uint16_t mod; /* (key down: 0x300 an Alt key) */ float dx, dy; /* (mouse motion, relative) */ + float x, y; /* (mouse motion, down and up: the cursor in the window, hos_sdl_window_size's units) */ + int button; /* (mouse down and up: 1 left, 2 middle, 3 right, as SDL numbers them) */ + float wheel; /* (mouse wheel: notches, up or away from the player positive) */ } HosSdlEvent; int hos_sdl_wait_event(HosSdlEvent *e, int timeout_ms); /* 1 when there was one */ int hos_sdl_poll_event(HosSdlEvent *e); diff --git a/host/posix/display_sdl.c b/host/posix/display_sdl.c index 65eaa2cfc..fe4e3a2f0 100644 --- a/host/posix/display_sdl.c +++ b/host/posix/display_sdl.c @@ -6,7 +6,9 @@ The keyboard's state goes to hos_posix_keys (GetAsyncKeyState, for nn_hid.c and the Insert menu), the window's focus and the screen's size to hos_posix_window (GetForegroundWindow, GetSystemMetrics). HOS_HEADLESS=1: no window. HOS_WINDOW_SIZE=x: its size (default 1280x720). F11: fullscreen (HOS_FULLSCREEN=1 to - start so). Mouse: a click in the window captures it (relative motion, for nn_hid.c), Esc or leaving releases it. */ + start so). Mouse: a click in the window captures it (relative motion, for nn_hid.c), Esc or leaving releases it. + While the Insert menu is open the mouse is free (the cursor shown, for the menu's clicks: hos_mouse_pointer) and + the game gets none of it; the capture it had comes back when the menu closes. */ #include "../hos.h" #include "../hos_sdl.h" @@ -39,6 +41,28 @@ void hos_mouse_give_back(int32_t dx, int32_t dy) { InterlockedExchangeAdd(&mouse_dy, dy); } +/* ---- the free mouse, for the Insert menu (gpu/vk/vk_api.h MkwVkPointer): set on the main thread, read on the renderer's ---- */ +static volatile LONG ptr_x, ptr_y, ptr_in, ptr_held, ptr_pressed, ptr_wheel; /* (x, y in window units; wheel in 1/120 notches) */ +static volatile int menu_open; /* (WM_APP + 8: the menu's state, as the window last heard it) */ +static void *window; +int hos_mouse_pointer(MkwVkPointer *out) { + memset(out, 0, sizeof *out); + if (!window || mouse_captured) return 0; + int ww = 0, wh = 0, pw = 0, ph = 0; + hos_sdl_window_size(window, &ww, &wh); /* (SDL's position is in the window's units: scaled to its pixels) */ + hos_sdl_pixel_size(window, &pw, &ph); + if (ww <= 0 || wh <= 0 || pw <= 0 || ph <= 0) return 0; + out->x = (int32_t)((int64_t)ptr_x * pw / ww), out->y = (int32_t)((int64_t)ptr_y * ph / wh); + out->width = pw, out->height = ph; + out->held = (uint32_t)ptr_held; + out->pressed = (uint32_t)InterlockedExchange(&ptr_pressed, 0); + const LONG wheel = InterlockedExchange(&ptr_wheel, 0); + out->wheel = wheel / 120; + if (wheel % 120) InterlockedExchangeAdd(&ptr_wheel, wheel % 120); /* (a part of a notch: kept for the next) */ + return ptr_in && hos_posix_window[0]; +} +static uint32_t sdl_button_bit(int b) { return b == 1 ? HOS_MOUSE_LEFT : b == 3 ? HOS_MOUSE_RIGHT : b == 2 ? HOS_MOUSE_MIDDLE : 0; } + /* ---- keys: SDL's scancodes (USB HID usages) as Windows' virtual keys ---- */ static void keys_update(void) { int n = 0; @@ -73,7 +97,6 @@ static void keys_update(void) { } /* ---- the window ---- */ -static void *window; static volatile int renderer; /* the renderer presents into the window */ static HANDLE renderer_up; /* (set once rx_start has returned, ready or not: hos_renderer_wait) */ static int fullscreen; @@ -120,11 +143,28 @@ void hos_renderer_wait(void) { static volatile int start_asked; static volatile int fullscreen_asked = -1; /* (the Insert menu's row, from the renderer's thread: applied on this one) */ +static volatile int menu_asked = -1; /* (the Insert menu opened or closed, likewise) */ -/* PostMessageA to the window (host/posix/win32_posix.c): WM_APP + 7, the Insert menu's Fullscreen row (host/display.c's) */ +/* PostMessageA to the window (host/posix/win32_posix.c): WM_APP + 7, the Insert menu's Fullscreen row, WM_APP + 8 the + menu opened or closed (host/display.c's) */ __attribute__((visibility("default"))) void hos_posix_post_message(uint32_t msg, uintptr_t wp, intptr_t lp) { (void)lp; if (msg == WM_APP + 7) __atomic_store_n(&fullscreen_asked, wp ? 1 : 0, __ATOMIC_RELEASE); + if (msg == WM_APP + 8) __atomic_store_n(&menu_asked, wp ? 1 : 0, __ATOMIC_RELEASE); +} +/* the menu opened: the mouse let go (its capture kept for later); closed: the capture back, if the window still has the focus */ +static int captured_before_menu; +static void menu_mouse(int open) { + if (open == menu_open) return; + menu_open = open; + if (open) { + captured_before_menu = (int)mouse_captured; + mouse_capture(0); + InterlockedExchange(&ptr_pressed, 0), InterlockedExchange(&ptr_wheel, 0); + } else { + if (captured_before_menu && hos_posix_window[0]) mouse_capture(1); + captured_before_menu = 0; + } } static int sdl_ready; int hos_display_owns_sdl(void) { return sdl_ready; } /* (controllers stay on this thread while it runs SDL) */ @@ -221,9 +261,14 @@ void hos_display_loop(void) { case HOS_SDL_EV_FOCUS_LOST: hos_posix_window[0] = 0; mouse_capture(0); + ptr_in = 0, ptr_held = 0; break; case HOS_SDL_EV_MOUSE_LEAVE: mouse_capture(0); + ptr_in = 0; + break; + case HOS_SDL_EV_MOUSE_ENTER: + ptr_in = 1; break; case HOS_SDL_EV_KEY_DOWN: { const int scancode = e.scancode, mod = e.mod; @@ -232,9 +277,20 @@ void hos_display_loop(void) { break; } case HOS_SDL_EV_MOUSE_DOWN: - if (renderer && !mouse_captured) mouse_capture(1); + ptr_x = (LONG)e.x, ptr_y = (LONG)e.y, ptr_in = 1; + InterlockedOr(&ptr_held, (LONG)sdl_button_bit(e.button)); + if (menu_open) InterlockedOr(&ptr_pressed, (LONG)sdl_button_bit(e.button)); /* (the menu's click) */ + else if (renderer && !mouse_captured) mouse_capture(1); + break; + case HOS_SDL_EV_MOUSE_UP: + ptr_x = (LONG)e.x, ptr_y = (LONG)e.y; + InterlockedAnd(&ptr_held, ~(LONG)sdl_button_bit(e.button)); + break; + case HOS_SDL_EV_MOUSE_WHEEL: + if (menu_open) InterlockedExchangeAdd(&ptr_wheel, (LONG)(e.wheel * 120)); break; case HOS_SDL_EV_MOUSE_MOTION: + if (!mouse_captured) ptr_x = (LONG)e.x, ptr_y = (LONG)e.y, ptr_in = 1; if (mouse_captured) { /* (SDL3's motion comes in fractions of a pixel: the remainders kept for the next) */ static float rx, ry; rx += e.dx, ry += e.dy; @@ -249,6 +305,8 @@ void hos_display_loop(void) { } const int asked = __atomic_exchange_n(&fullscreen_asked, -1, __ATOMIC_ACQ_REL); if (asked >= 0) set_fullscreen(asked); + const int menu = __atomic_exchange_n(&menu_asked, -1, __ATOMIC_ACQ_REL); + if (menu >= 0) menu_mouse(menu); keys_update(); } } diff --git a/host/rx_backend.c b/host/rx_backend.c index ebd912a93..5edbbbcb1 100644 --- a/host/rx_backend.c +++ b/host/rx_backend.c @@ -134,13 +134,14 @@ static uint64_t host_guest_alloc(uint64_t size) { void hos_watch_guest_writes(const uint8_t *data, uint64_t size); /* nvn_native.c */ static void host_watch_guest_writes(uint64_t guest, uint64_t size) { hos_watch_guest_writes(hos_guest_base() + guest, size); } static float host_aspect_ratio(void) { return hos_aspect_ratio; } +int hos_mouse_pointer(MkwVkPointer *out); /* display.c, posix/display_sdl.c */ static const MkwVkHost vk_host = { MKWVK_API_VERSION, host_log, hos_guest_base, hos_guest_size, hos_gpu_ptr, nvn_gpu_guest, nvn_gpu_read, nvn_gpu_write, host_collect, host_free_pages, host_mark_written, hos_ticks, hos_now_ns, nvn_tex_layout, nvn_tex_level_offset, nvn_fmt_bpb, nvn_tex_level_tiles, nvn_tex_linear_stride, nvn_depth_minus_w, hos_gpu_pools, hos_gpu_maps_generation, nvn_vmap_generation, hos_reserved_cpus, host_collect_in, host_guest_alloc, - host_watch_guest_writes, hos_pad_raw, hos_binding, hos_bind, host_aspect_ratio, + host_watch_guest_writes, hos_pad_raw, hos_binding, hos_bind, host_aspect_ratio, hos_mouse_pointer, }; static const MkwVkRenderer *vk; /* the Vulkan back end, when it was chosen and started */ diff --git a/tests/run_all.py b/tests/run_all.py index 36e92615d..0e3cfcfae 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -32,6 +32,7 @@ "tests/test_lower_depbar.py": ["--cxx", "clang++"], "tests/test_text_speed.py": ["--cc", "gcc"], "tests/test_upscale_fallback.py": ["--cxx", "g++"], + "tests/test_menu_hit.py": ["--cxx", "g++"], "tests/test_nvn_buffer_map.py": ["--cc", "gcc"], "tests/test_cpp_preflight.py": ["--cxx", "g++"], "tests/test_invalidate_vcore.py": ["--cxx", "g++"], diff --git a/tests/test_menu_hit.cpp b/tests/test_menu_hit.cpp new file mode 100644 index 000000000..bfd19b94e --- /dev/null +++ b/tests/test_menu_hit.cpp @@ -0,0 +1,133 @@ +// The Insert menu's hit-test (gpu/vk/vk_menu_hit.h): where a mouse position falls on the menu, at a scale of 1 (a +// 720-line window) and 2 (1440 lines), and through a window whose pixels are not the swapchain's. +#include "../gpu/vk/vk_menu_hit.h" + +#include + +#define CHECK(name, condition) do { if (!(condition)) { std::fprintf(stderr, "FAIL %s at line %d\n", name, __LINE__); return 1; } } while (0) + +using namespace mkwvk::menu_hit; + +int main() { + // the Graphics tab: 11 rows, the Sharpness row (7) a slider; values from x 380 at scale 1 + int value_l[11]; + bool slider[11] = {}; + for (int& v : value_l) v = 380; + slider[7] = true; + const int w1 = kWidth, h1 = 520; + auto at = [&](int x, int y, int s) { + int vl[11]; + for (int i = 0; i < 11; i++) vl[i] = value_l[i] * s; + return options(x, y, s, kWidth * s, h1 * s, 3, 11, vl, slider); + }; + Hit h; + + // ---- scale 1: the tabs (side 85, slots of 103 from 85) ---- + h = at(100, 60, 1); + CHECK("tab 0", h.part == kTab && h.index == 0); + h = at(240, 60, 1); + CHECK("tab 1", h.part == kTab && h.index == 1); + h = at(300, 60, 1); + CHECK("tab 2", h.part == kTab && h.index == 2); + CHECK("[Q / LB] is the previous tab", at(40, 60, 1).part == kTabPrev); + CHECK("[E / RB] is the next tab", at(450, 60, 1).part == kTabNext); + CHECK("the title is no control", at(100, 20, 1).part == kNone); + + // ---- scale 1: the rows (from 84, 28 high) ---- + h = at(30, 84 + 3 * 28 + 10, 1); + CHECK("row 3 by its name", h.part == kRow && h.index == 3 && h.side == 0); + h = at(390, 84 + 3 * 28 + 10, 1); + CHECK("row 3's value, left half", h.part == kRow && h.index == 3 && h.side == -1); + h = at(450, 84 + 3 * 28 + 10, 1); + CHECK("row 3's value, right half", h.part == kRow && h.index == 3 && h.side == 1); + h = at(30, 84, 1); + CHECK("row 0's top line", h.part == kRow && h.index == 0); + h = at(30, 84 - 1, 1); + CHECK("above the rows", h.part == kNone); + CHECK("the row band's left margin", at(7, 84 + 10, 1).part == kNone); + CHECK("past the last row", at(30, 84 + 11 * 28 + 2, 1).part == kNone); + CHECK("a tab with fewer rows", options(30, 84 + 5 * 28 + 2, 1, w1, h1, 3, 3, value_l, nullptr).part == kNone); + + // ---- scale 1: the slider's track, 200 to 330 ---- + const int sy = 84 + 7 * 28 + 14; + h = at(265, sy, 1); + CHECK("the track's middle", h.part == kSlider && h.index == 7 && h.permille == 500); + h = at(196, sy, 1); + CHECK("just before the track: its start", h.part == kSlider && h.permille == 0); + h = at(334, sy, 1); + CHECK("just past the track: its end", h.part == kSlider && h.permille == 1000); + h = at(340, sy, 1); + CHECK("past the grab: the row's name", h.part == kRow && h.side == 0); + h = at(265, 84 + 6 * 28 + 14, 1); + CHECK("no track on a row without one", h.part == kRow && h.index == 6); + CHECK("a drag past the track holds to its ends", slider_drag(100, 1) == 0 && slider_drag(500, 1) == 1000); + CHECK("a drag along it", slider_drag(265, 1) == 500); + + // ---- the slider's values ---- + CHECK("sharpness at half", slider_value(500, 0, 100, 10) == 50); + CHECK("sharpness ends", slider_value(0, 0, 100, 10) == 0 && slider_value(1000, 0, 100, 10) == 100); + CHECK("sensitivity at half (nearest step)", slider_value(500, 10, 500, 10) == 260); + CHECK("sensitivity ends", slider_value(0, 10, 500, 10) == 10 && slider_value(1000, 10, 500, 10) == 500); + CHECK("knob place", slider_permille(50, 0, 100) == 500 && slider_permille(10, 10, 500) == 0 && + slider_permille(500, 10, 500) == 1000); + CHECK("a value and its knob agree", slider_value(slider_permille(260, 10, 500), 10, 500, 10) == 260); + + // ---- scale 2 (a 1440-line window): everything twice as far ---- + h = at(480, 120, 2); + CHECK("scale 2: tab 1", h.part == kTab && h.index == 1); + CHECK("scale 2: [Q / LB]", at(100, 120, 2).part == kTabPrev); + h = at(60, 168 + 2 * 56 + 5, 2); + CHECK("scale 2: row 2", h.part == kRow && h.index == 2 && h.side == 0); + h = at(900, 168 + 2 * 56 + 5, 2); + CHECK("scale 2: row 2's value, right half", h.part == kRow && h.side == 1); + h = at(530, 168 + 7 * 56 + 28, 2); + CHECK("scale 2: the track's middle", h.part == kSlider && h.permille == 500); + + // ---- off the menu ---- + CHECK("left of the menu", at(-1, 100, 1).part == kOutside); + CHECK("right of the menu", at(w1, 100, 1).part == kOutside); + CHECK("below the menu", at(100, h1, 1).part == kOutside); + + // ---- the window's pixels to the menu's: a 1280x720 window, the menu centred ---- + { + int ix, iy, x0, y0; + to_image(400 + 300, 100 + 60, 1280, 720, 1280, 720, &ix, &iy); + origin(1280, 720, w1, h1, &x0, &y0); + CHECK("centred at 1280x720", x0 == 400 && y0 == 100); + h = options(ix - x0, iy - y0, 1, w1, h1, 3, 11, value_l, slider); + CHECK("1280x720: tab 2", h.part == kTab && h.index == 2); + } + { // (the swapchain at 2560x1440 while the window still says 1280x720 pixels: a resize on its way) + int ix, iy, x0, y0; + to_image(640, 160, 1280, 720, 2560, 1440, &ix, &iy); + CHECK("window to image", ix == 1280 && iy == 320); + origin(2560, 1440, kWidth * 2, h1 * 2, &x0, &y0); + h = at(ix - x0, iy - y0, 2); + CHECK("2560x1440: tab 1", h.part == kTab && h.index == 1); + } + { // (a 1920x1080 window: still scale 1, centred) + int x0, y0; + origin(1920, 1080, w1, h1, &x0, &y0); + h = options(960 - x0, (y0 + 84 + 28 + 5) - y0, 1, w1, h1, 3, 11, value_l, slider); + CHECK("1920x1080: row 1", h.part == kRow && h.index == 1); + } + + // ---- the Controls page: 24 controls, then Reset; rows 24 high from 64 ---- + const int cw = kCtlWidth, ch = 720; + h = controls(50, 64 + 2 * 24 + 5, 1, cw, ch, 24); + CHECK("controls: row 2's name", h.part == kCtlRow && h.index == 2); + h = controls(250, 64 + 2 * 24 + 5, 1, cw, ch, 24); + CHECK("controls: row 2's gamepad", h.part == kCtlCell && h.index == 2 && h.side == 0); + h = controls(400, 64 + 2 * 24 + 5, 1, cw, ch, 24); + CHECK("controls: row 2's keys", h.part == kCtlCell && h.index == 2 && h.side == 1); + h = controls(193, 64 + 5, 1, cw, ch, 24); + CHECK("controls: just before the gamepad's cell", h.part == kCtlRow && h.index == 0); + h = controls(250, 64 + 24 * 24 + 5, 1, cw, ch, 24); + CHECK("controls: Reset to defaults is a row", h.part == kCtlRow && h.index == 24); + CHECK("controls: past Reset", controls(250, 64 + 25 * 24 + 5, 1, cw, ch, 24).part == kNone); + CHECK("controls: the header", controls(250, 50, 1, cw, ch, 24).part == kNone); + h = controls(800, 128 + 2 * 48 + 10, 2, cw * 2, ch * 2, 24); + CHECK("controls, scale 2: row 2's keys", h.part == kCtlCell && h.index == 2 && h.side == 1); + + std::puts("menu hit-test: all checks passed"); +} diff --git a/tests/test_menu_hit.py b/tests/test_menu_hit.py new file mode 100644 index 000000000..d7ccca678 --- /dev/null +++ b/tests/test_menu_hit.py @@ -0,0 +1,62 @@ +"""The Insert menu's mouse: the hit-test (gpu/vk/vk_menu_hit.h) compiled and run against positions at two window sizes +(tests/test_menu_hit.cpp), and the sources checked for the rest of the path: the window lets the mouse go while the +menu is open and takes it back after (WM_APP + 8, host/display.c and host/posix/display_sdl.c), the free mouse reaches +the menu (MkwVkHost.pointer, host/rx_backend.c), and the menu draws with the sizes it hit-tests with. + + python -I -B tests/test_menu_hit.py [--cxx g++] +""" +import argparse +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def read(*parts): + return ROOT.joinpath(*parts).read_text(encoding="utf-8") + + +def sources(): + menu, api, backend = read("gpu", "vk", "vk_menu.cpp"), read("gpu", "vk", "vk_api.h"), read("host", "rx_backend.c") + win, sdl = read("host", "display.c"), read("host", "posix", "display_sdl.c") + if '#include "vk_menu_hit.h"' not in menu or "menu_hit::options(" not in menu or "menu_hit::controls(" not in menu: + raise SystemExit("the menu does not hit-test with vk_menu_hit.h") + for size in ("menu_hit::kTabY", "menu_hit::kRowH", "menu_hit::rows_top(s)", "menu_hit::tab_slot_w(", "menu_hit::kCtlRowH"): + if size not in menu: + raise SystemExit("the menu draws without %s: its picture and its hit-test can drift apart" % size) + if "PostMessageA(app_window(), WM_APP + 8" not in menu: + raise SystemExit("the menu does not tell the window it opened or closed") + if "int (*pointer)(MkwVkPointer *out);" not in api or "hos_mouse_pointer," not in backend: + raise SystemExit("the free mouse does not reach the renderer") + for name, src in (("display.c", win), ("display_sdl.c", sdl)): + if "int hos_mouse_pointer(MkwVkPointer *out)" not in src: + raise SystemExit(name + ": no hos_mouse_pointer") + if "WM_APP + 8" not in src or "captured_before_menu" not in src: + raise SystemExit(name + ": the menu does not release the mouse and give the capture back") + # (a click with the menu open is the menu's: it never captures the mouse for the game) + down = sdl[sdl.index("case HOS_SDL_EV_MOUSE_DOWN:"):] + down = down[:down.index("break;")] + if "if (menu_open)" not in down or "else if (renderer && !mouse_captured) mouse_capture(1);" not in down: + raise SystemExit("display_sdl.c: a click with the menu open captures the mouse") + down = win[win.index("case WM_LBUTTONDOWN:"):] + down = down[:down.index("return 0;")] + if "if (menu_open)" not in down or "else if (renderer && !mouse_captured) mouse_capture(w, 1);" not in down: + raise SystemExit("display.c: a click with the menu open captures the mouse") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cxx", default="g++") + args = parser.parse_args() + sources() + with tempfile.TemporaryDirectory(prefix="menu-hit-") as tmp: + exe = Path(tmp) / "menu-hit" + subprocess.run([args.cxx, "-std=c++20", "-Wall", "-Wextra", "-Werror", str(ROOT / "tests" / "test_menu_hit.cpp"), + "-o", str(exe)], check=True) + subprocess.run([str(exe)], check=True) + print("PASS: the menu's mouse") + + +if __name__ == "__main__": + main()