From 33c2cf60cc656f9611639fae3633c31529b16375 Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:43:34 +0200 Subject: [PATCH] Fix overlay input leaking into the game: explicit input ownership Interacting with the overlays also drove the game (clicks fired weapons, scroll cycled them, keys leaked), and hovering one silenced a real pad. Input now follows window-manager rules, mouse to the window under the cursor, keyboard to the focused window, real pads never suppressed, via ImGui capture flags conjoined with overlay visibility, published as atomics. --- src/ac6_backend_fixes/ac6_kbm_input.cpp | 348 +++++++++++------- src/ac6_native_graphics_overlay.cpp | 6 +- src/ac6_native_graphics_overlay.h | 7 +- .../include/rex/input/input_driver.h | 7 + .../include/rex/input/mnk/mnk_input_driver.h | 4 + .../rexglue-sdk/include/rex/ui/imgui_dialog.h | 7 + .../rexglue-sdk/include/rex/ui/imgui_drawer.h | 27 ++ .../rex/ui/overlay/build_stamp_overlay.h | 7 + .../include/rex/ui/overlay/console_overlay.h | 2 +- .../include/rex/ui/overlay/debug_overlay.h | 2 +- .../include/rex/ui/overlay/settings_overlay.h | 2 +- .../rexglue-sdk/src/input/input_system.cpp | 11 +- .../rexglue-sdk/src/native/ui/rex_app.cpp | 11 +- .../rexglue-sdk/src/ui/imgui_drawer.cpp | 38 ++ .../src/ui/overlay/settings_overlay.cpp | 5 + 15 files changed, 336 insertions(+), 148 deletions(-) diff --git a/src/ac6_backend_fixes/ac6_kbm_input.cpp b/src/ac6_backend_fixes/ac6_kbm_input.cpp index 5ebc4f18..7840c4de 100644 --- a/src/ac6_backend_fixes/ac6_kbm_input.cpp +++ b/src/ac6_backend_fixes/ac6_kbm_input.cpp @@ -47,13 +47,13 @@ #include #include -#include #include #include #include #include #include +#include #include #include @@ -559,16 +559,42 @@ void EnforceMnkOff() { } } -// ---- Host keyboard state ---------------------------------------------------- +// Mouse-steering state (used by MouseSteerPoll below). Declared here because +// the input gate consults `capturing`: while steering pins the cursor it OWNS +// the pointer, and overlay mouse ownership is suspended. +struct MouseSteer { + double x = 0.0, y = 0.0; // position model: virtual stick, -1..1 + double rate_x = 0.0, rate_y = 0.0; // velocity model: filtered px/s + double cam_x = 0.0, cam_y = 0.0; // camera-control mode: held RS deflection + bool capturing = false; + int64_t last_ms = 0; +}; +MouseSteer g_mouse; + +// ---- Host input gates ------------------------------------------------------- +// Input ownership follows desktop window-manager rules: the MOUSE belongs to +// whatever is under the CURSOR, the KEYBOARD to whatever holds FOCUS. The +// ImGuiDrawer publishes once per frame whether any visible overlay +// window owns the pointer / the keyboard / an active text field - its ImGui +// capture flags conjoined with overlay visibility, so a CLOSED overlay can +// never own input by construction (the focus latch that got a raw +// WantCaptureKeyboard gate rejected here previously cannot form). +// One rule on top, also window-manager semantics (pointer capture): while +// mouse steering holds the pointer - pinned to the window center and hidden - +// there is no cursor to hover an overlay with, so the mouse stays with the +// game until steering releases it (menus, pause, mode "off", focus loss). +// Pad input is deliberately NOT gated: the overlays are not pad-navigable +// (the drawer feeds ImGui keyboard/mouse/touch only), so no overlay can own +// pad input and gating it would change behaviour with no owner on the other +// side. struct GateState { bool fg_ok = false; - bool imgui_ctx = false; - bool want_text = false; - bool want_capture = false; - // Block only on an active text field: WantCaptureKeyboard proved too broad - // a signal to gate on (it can be latched by overlay focus), so it is logged - // for diagnosis but does not gate. - bool open() const { return fg_ok && !want_text; } + bool capture_mouse = false; // a visible overlay owns the pointer + bool capture_keyboard = false; // a visible overlay owns the keyboard + bool want_text = false; // an overlay text field is active + bool steer_owns_pointer = false; // mouse steering is pinning the cursor + bool keys_ok() const { return fg_ok && !capture_keyboard && !want_text; } + bool mouse_ok() const { return fg_ok && (steer_owns_pointer || !capture_mouse); } }; GateState QueryGate() { @@ -583,15 +609,33 @@ GateState QueryGate() { #else g.fg_ok = true; #endif - if (ImGui::GetCurrentContext() != nullptr) { - g.imgui_ctx = true; - const ImGuiIO& io = ImGui::GetIO(); - g.want_text = io.WantTextInput; - g.want_capture = io.WantCaptureKeyboard; - } + g.capture_mouse = rex::ui::ImGuiDrawer::DialogsCaptureMouse(); + g.capture_keyboard = rex::ui::ImGuiDrawer::DialogsCaptureKeyboard(); + g.want_text = rex::ui::ImGuiDrawer::DialogsWantTextInput(); + g.steer_owns_pointer = g_mouse.capturing; return g; } +// LMB/RMB/MMB/X1/X2 and the synthetic wheel keys are MOUSE input (ownership +// follows the cursor); every other key is KEYBOARD input (ownership follows +// focus). The two kinds gate independently, like windows on a desktop. +bool IsMouseKey(VirtualKey vk) { + switch (vk) { + case VirtualKey::kLButton: + case VirtualKey::kRButton: + case VirtualKey::kMButton: + case VirtualKey::kXButton1: + case VirtualKey::kXButton2: + return true; + default: + return vk == kVkWheelUp || vk == kVkWheelDown; + } +} + +bool KeyAllowed(const GateState& gate, VirtualKey vk) { + return IsMouseKey(vk) ? gate.mouse_ok() : gate.keys_ok(); +} + bool KeyHeld(VirtualKey vk) { if (vk == kVkWheelUp) { return NowMs() < g_wheel_up_until.load(std::memory_order_relaxed); @@ -660,19 +704,11 @@ void SetCursorHidden(bool, void*) {} #endif // ---- Mouse steering (M3) ----------------------------------------------------- -// Virtual left stick fed by raw cursor deltas. While flying with mode=steer -// and the input gate open, the OS cursor is pinned to the game window's -// center each poll and the deltas accumulate into a clamped stick position -// with config-driven feel. Keyboard pitch/roll overrides its axis. -struct MouseSteer { - double x = 0.0, y = 0.0; // position model: virtual stick, -1..1 - double rate_x = 0.0, rate_y = 0.0; // velocity model: filtered px/s - double cam_x = 0.0, cam_y = 0.0; // camera-control mode: held RS deflection - bool capturing = false; - int64_t last_ms = 0; -}; -MouseSteer g_mouse; - +// Virtual left stick fed by raw cursor deltas (state in MouseSteer above). +// While flying with mode=steer and the mouse owned by the game, the OS cursor +// is pinned to the game window's center each poll and the deltas accumulate +// into a clamped stick position with config-driven feel. Keyboard pitch/roll +// overrides its axis. void MouseSteerRelease() { g_mouse.capturing = false; g_mouse.x = g_mouse.y = 0.0; @@ -844,11 +880,11 @@ void DumpMaskTables(uint8_t* base, uint32_t singleton, uint32_t fifth) { } } -uint32_t GatherMirrorBits() { +uint32_t GatherMirrorBits(const GateState& gate) { uint32_t mirror = 0; for (size_t i = 0; i < kNumMenuActions; ++i) { for (VirtualKey vk : g_config.menu_keys[i]) { - if (KeyHeld(vk)) { + if (KeyAllowed(gate, vk) && KeyHeld(vk)) { mirror |= 1u << kMenuActions[i].mirror_bit; break; } @@ -857,12 +893,14 @@ uint32_t GatherMirrorBits() { return mirror; } -void InjectMenu(uint8_t* base, uint32_t inst, double dt, bool gate_open, - uint32_t kb_mirror) { +void InjectMenu(uint8_t* base, uint32_t inst, double dt, uint32_t kb_mirror) { // Translate mirror-space presses into THIS instance's action bits via its // own binding masks - bit-exact with what a real pad press produces here. + // kb_mirror is already input-ownership-gated by GatherMirrorBits; a gate + // closing mid-hold arrives as kb_mirror dropping to 0, which produces the + // released-edge below - keys never stick when an overlay takes the input. uint32_t level = 0; - if (gate_open && kb_mirror) { + if (kb_mirror) { for (int a = 0; a < 32; ++a) { if (ActionMask(base, inst, a) & kb_mirror) { level |= 1u << a; @@ -1007,7 +1045,7 @@ PPC_EXTERN_FUNC(__imp__rex_sub_82390CE0); // guest XamInputGetState(user,0,stat // right here at the XamInputGetState boundary as XINPUT button bits: every // downstream layer then sees keyboard presses exactly as pad presses. // (Flight/M2 will gate this by game mode so flight keys never collide.) -uint32_t GatherXInputBits() { +uint32_t GatherXInputBits(const GateState& gate) { static const uint16_t kXBits[kNumMenuActions] = { 0x0001, // up -> DPAD_UP 0x0002, // down -> DPAD_DOWN @@ -1021,7 +1059,7 @@ uint32_t GatherXInputBits() { uint32_t bits = 0; for (size_t i = 0; i < kNumMenuActions; ++i) { for (VirtualKey vk : g_config.menu_keys[i]) { - if (KeyHeld(vk)) { + if (KeyAllowed(gate, vk) && KeyHeld(vk)) { bits |= kXBits[i]; break; } @@ -1053,112 +1091,123 @@ PPC_FUNC_IMPL(rex_sub_82390CE0) { // Device-level keyboard injection (user 0 only, successful polls only). // Context switch: flying -> [flight] key set; everything else -> [menu] set. + // Ownership gating is per input KIND (KeyAllowed): keyboard keys drop out + // while an overlay holds keyboard focus, mouse buttons and the wheel while + // the cursor is over an overlay. A gate closing mid-hold simply stops + // asserting the bits - downstream edge detection sees a clean release. if (REXCVAR_GET(ac6_kbm_enabled) && user == 0 && state_ptr != 0 && ctx.r3.u32 == 0) { const GateState gate = QueryGate(); - if (gate.open()) { - if (FlightActive()) { - uint16_t btn = 0; - uint8_t lt = 0, rt = 0; - int32_t lx = 0, ly = 0, rx = 0, ry = 0; - for (size_t i = 0; i < kNumFlightActions; ++i) { - for (VirtualKey vk : g_config.flight_keys[i]) { - if (KeyHeld(vk)) { - const FlightActionDef& d = kFlightActions[i]; - btn |= d.buttons; - if (d.lt > lt) lt = d.lt; - if (d.rt > rt) rt = d.rt; - lx += d.lx; - ly += d.ly; - rx += d.rx; - ry += d.ry; - break; - } - } - } - // AC7-style camera control key: while held, the mouse drives the - // camera (right stick) instead of pitch/roll. - bool cam_mode = false; - for (VirtualKey vk : g_config.flight_keys[CameraControlAction()]) { - if (KeyHeld(vk)) { - cam_mode = true; + if (FlightActive()) { + uint16_t btn = 0; + uint8_t lt = 0, rt = 0; + int32_t lx = 0, ly = 0, rx = 0, ry = 0; + for (size_t i = 0; i < kNumFlightActions; ++i) { + for (VirtualKey vk : g_config.flight_keys[i]) { + if (KeyAllowed(gate, vk) && KeyHeld(vk)) { + const FlightActionDef& d = kFlightActions[i]; + btn |= d.buttons; + if (d.lt > lt) lt = d.lt; + if (d.rt > rt) rt = d.rt; + lx += d.lx; + ly += d.ly; + rx += d.rx; + ry += d.ry; break; } } - - // Mouse steering: runs every flight poll (maintains capture/recenter); - // keyboard keys override the mouse per axis, pad stick wins when both idle. - // With the camera key held, mx/my are the held camera deflection instead. - double mx = 0.0, my = 0.0; - const bool mouse_on = MouseSteerPoll(cam_mode, mx, my); - - const bool any = btn || lt || rt || lx != 0 || ly != 0 || rx != 0 || ry != 0 || - (mouse_on && (mx != 0.0 || my != 0.0)); - if (any) { - const uint16_t cur = rex::memory::load_and_swap(base + state_ptr + 4); - rex::memory::store_and_swap(base + state_ptr + 4, - static_cast(cur | btn)); - auto max_u8 = [&](uint32_t off, uint8_t v) { - uint8_t c = *(base + state_ptr + off); - if (v > c) *(base + state_ptr + off) = v; - }; - max_u8(6, lt); - max_u8(7, rt); - auto clamp16 = [](int32_t v) { - return static_cast(v > 32767 ? 32767 : (v < -32767 ? -32767 : v)); - }; - if (lx != 0) { - rex::memory::store_and_swap(base + state_ptr + 8, clamp16(lx)); - } else if (mouse_on && !cam_mode && mx != 0.0) { - rex::memory::store_and_swap(base + state_ptr + 8, - clamp16(static_cast(mx * 32767.0))); - } - // Mouse "pull" (my positive) = stick pulled = negative LY, matching - // the pitch_up fallback key. - if (ly != 0) { - rex::memory::store_and_swap(base + state_ptr + 10, clamp16(ly)); - } else if (mouse_on && !cam_mode && my != 0.0) { - rex::memory::store_and_swap(base + state_ptr + 10, - clamp16(static_cast(-my * 32767.0))); - } - if (rx != 0) { - rex::memory::store_and_swap(base + state_ptr + 12, clamp16(rx)); - } else if (mouse_on && cam_mode && mx != 0.0) { - rex::memory::store_and_swap(base + state_ptr + 12, - clamp16(static_cast(mx * 32767.0))); - } - if (ry != 0) { - rex::memory::store_and_swap(base + state_ptr + 14, clamp16(ry)); - } else if (mouse_on && cam_mode && my != 0.0) { - rex::memory::store_and_swap(base + state_ptr + 14, - clamp16(static_cast(my * 32767.0))); - } - static int s_lines = 0; - static uint32_t s_last = 0; - const uint32_t sig = btn | (lt << 16) | (rt << 24) | ((lx != 0) << 30) | - ((ly != 0) << 31); - if (REXCVAR_GET(ac6_kbm_log) && sig != s_last && s_lines < 100) { - s_last = sig; - ++s_lines; - KbmLog(fmt::format("xinput inject FLIGHT btn=0x{:04X} lt={} rt={} lx={} ly={} " - "mouse={} cam={} mx={:.2f} my={:.2f}", - btn, lt, rt, lx, ly, mouse_on, cam_mode, mx, my)); - } + } + // AC7-style camera control key: while held, the mouse drives the + // camera (right stick) instead of pitch/roll. + bool cam_mode = false; + for (VirtualKey vk : g_config.flight_keys[CameraControlAction()]) { + if (KeyAllowed(gate, vk) && KeyHeld(vk)) { + cam_mode = true; + break; } + } + + // Mouse steering: runs every flight poll while the game owns the + // pointer (maintains capture/recenter); keyboard keys override the + // mouse per axis, pad stick wins when both idle. With the camera key + // held, mx/my are the held camera deflection instead. If a visible + // overlay owns the pointer (cursor hovering it while free), steering + // does not acquire - the cursor stays live for the overlay until it + // moves off, exactly like a desktop window under the pointer. + double mx = 0.0, my = 0.0; + bool mouse_on = false; + if (gate.mouse_ok()) { + mouse_on = MouseSteerPoll(cam_mode, mx, my); } else { MouseSteerRelease(); - const uint32_t kb = GatherXInputBits(); - if (kb != 0) { - const uint16_t cur = rex::memory::load_and_swap(base + state_ptr + 4); - rex::memory::store_and_swap(base + state_ptr + 4, - static_cast(cur | kb)); - static int s_lines = 0; - static uint32_t s_last = 0; - if (REXCVAR_GET(ac6_kbm_log) && kb != s_last && s_lines < 100) { - s_last = kb; - ++s_lines; - KbmLog(fmt::format("xinput inject wButtons 0x{:04X}", kb)); - } + } + + const bool any = btn || lt || rt || lx != 0 || ly != 0 || rx != 0 || ry != 0 || + (mouse_on && (mx != 0.0 || my != 0.0)); + if (any) { + const uint16_t cur = rex::memory::load_and_swap(base + state_ptr + 4); + rex::memory::store_and_swap(base + state_ptr + 4, + static_cast(cur | btn)); + auto max_u8 = [&](uint32_t off, uint8_t v) { + uint8_t c = *(base + state_ptr + off); + if (v > c) *(base + state_ptr + off) = v; + }; + max_u8(6, lt); + max_u8(7, rt); + auto clamp16 = [](int32_t v) { + return static_cast(v > 32767 ? 32767 : (v < -32767 ? -32767 : v)); + }; + if (lx != 0) { + rex::memory::store_and_swap(base + state_ptr + 8, clamp16(lx)); + } else if (mouse_on && !cam_mode && mx != 0.0) { + rex::memory::store_and_swap(base + state_ptr + 8, + clamp16(static_cast(mx * 32767.0))); + } + // Mouse "pull" (my positive) = stick pulled = negative LY, matching + // the pitch_up fallback key. + if (ly != 0) { + rex::memory::store_and_swap(base + state_ptr + 10, clamp16(ly)); + } else if (mouse_on && !cam_mode && my != 0.0) { + rex::memory::store_and_swap(base + state_ptr + 10, + clamp16(static_cast(-my * 32767.0))); + } + if (rx != 0) { + rex::memory::store_and_swap(base + state_ptr + 12, clamp16(rx)); + } else if (mouse_on && cam_mode && mx != 0.0) { + rex::memory::store_and_swap(base + state_ptr + 12, + clamp16(static_cast(mx * 32767.0))); + } + if (ry != 0) { + rex::memory::store_and_swap(base + state_ptr + 14, clamp16(ry)); + } else if (mouse_on && cam_mode && my != 0.0) { + rex::memory::store_and_swap(base + state_ptr + 14, + clamp16(static_cast(my * 32767.0))); + } + static int s_lines = 0; + static uint32_t s_last = 0; + const uint32_t sig = btn | (lt << 16) | (rt << 24) | ((lx != 0) << 30) | + ((ly != 0) << 31); + if (REXCVAR_GET(ac6_kbm_log) && sig != s_last && s_lines < 100) { + s_last = sig; + ++s_lines; + KbmLog(fmt::format("xinput inject FLIGHT btn=0x{:04X} lt={} rt={} lx={} ly={} " + "mouse={} cam={} mx={:.2f} my={:.2f}", + btn, lt, rt, lx, ly, mouse_on, cam_mode, mx, my)); + } + } + } else { + MouseSteerRelease(); + const uint32_t kb = GatherXInputBits(gate); + if (kb != 0) { + const uint16_t cur = rex::memory::load_and_swap(base + state_ptr + 4); + rex::memory::store_and_swap(base + state_ptr + 4, + static_cast(cur | kb)); + static int s_lines = 0; + static uint32_t s_last = 0; + if (REXCVAR_GET(ac6_kbm_log) && kb != s_last && s_lines < 100) { + s_last = kb; + ++s_lines; + KbmLog(fmt::format("xinput inject wButtons 0x{:04X}", kb)); } } } @@ -1299,24 +1348,45 @@ PPC_FUNC_IMPL(rex_sub_82211E28) { DumpMaskTables(base, singleton, s_fifth_inst); } - const uint32_t kb_mirror = GatherMirrorBits(); + const uint32_t kb_mirror = GatherMirrorBits(gate); + + // Gate transitions (capped): one line whenever input ownership changes - + // the direct log evidence for the overlay open/close and hover checks. + if (REXCVAR_GET(ac6_kbm_log)) { + static int s_gate_lines = 0; + static uint32_t s_gate_last = 0xFF; + const uint32_t sig = (gate.fg_ok << 0) | (gate.capture_mouse << 1) | + (gate.capture_keyboard << 2) | (gate.want_text << 3) | + (gate.steer_owns_pointer << 4); + if (sig != s_gate_last && s_gate_lines < 200) { + s_gate_last = sig; + ++s_gate_lines; + KbmLog(fmt::format( + "gate change: fg={} capMouse={} capKb={} text={} steerOwn={} -> keys={} mouse={}", + gate.fg_ok, gate.capture_mouse, gate.capture_keyboard, gate.want_text, + gate.steer_owns_pointer, gate.keys_ok(), gate.mouse_ok())); + } + } // Heartbeat: proves the hook runs, shows the gate, and shows raw key // detection INDEPENDENT of the gate (so a closed gate is visible too). if (REXCVAR_GET(ac6_kbm_log) && (call == 5 || (call % 3000) == 0)) { + GateState raw; // all-open gate: rawMirror = physical key detection + raw.fg_ok = true; KbmLog(fmt::format( "alive call={} inst=0x{:08X} singleton=0x{:08X} fifth@0x{:08X} dt={:.4f} " - "gate[fg={} imgui={} text={} capture={} open={}] targets=[{}] rawMirror=0x{:X} " - "flight={}", - call, inst, singleton, s_fifth_inst, dt, gate.fg_ok, gate.imgui_ctx, gate.want_text, - gate.want_capture, gate.open(), fmt::join(g_config.menu_instances, ","), kb_mirror, - FlightActive())); + "gate[fg={} capMouse={} capKb={} text={} steerOwn={} keys={} mouse={}] targets=[{}] " + "rawMirror=0x{:X} gatedMirror=0x{:X} flight={}", + call, inst, singleton, s_fifth_inst, dt, gate.fg_ok, gate.capture_mouse, + gate.capture_keyboard, gate.want_text, gate.steer_owns_pointer, gate.keys_ok(), + gate.mouse_ok(), fmt::join(g_config.menu_instances, ","), GatherMirrorBits(raw), + kb_mirror, FlightActive())); } if (!FlightActive()) { for (int target : g_config.menu_instances) { if (target == idx && idx != 2) { - InjectMenu(base, inst, dt, gate.open(), kb_mirror); + InjectMenu(base, inst, dt, kb_mirror); break; } } diff --git a/src/ac6_native_graphics_overlay.cpp b/src/ac6_native_graphics_overlay.cpp index 8c4e74ee..3d18f84f 100644 --- a/src/ac6_native_graphics_overlay.cpp +++ b/src/ac6_native_graphics_overlay.cpp @@ -16,12 +16,16 @@ NativeGraphicsStatusDialog::NativeGraphicsStatusDialog(rex::ui::ImGuiDrawer* img NativeGraphicsStatusDialog::~NativeGraphicsStatusDialog() = default; +bool NativeGraphicsStatusDialog::IsVisible() const { + return visible_ && !REXCVAR_GET(ac6_performance_mode); +} + void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) { (void)io; ApplyAc6PerformanceModeOverridesPublic(); - if (REXCVAR_GET(ac6_performance_mode) || !visible_) { + if (!IsVisible()) { return; } diff --git a/src/ac6_native_graphics_overlay.h b/src/ac6_native_graphics_overlay.h index 4de30238..b233eedc 100644 --- a/src/ac6_native_graphics_overlay.h +++ b/src/ac6_native_graphics_overlay.h @@ -17,7 +17,12 @@ class NativeGraphicsStatusDialog final : public rex::ui::ImGuiDialog { void Show() { visible_ = true; } void ToggleVisible() { visible_ = !visible_; } - bool IsVisible() const { return visible_; } + // Effective visibility, not just the toggle: performance mode suppresses + // the window entirely (see OnDraw), and a window that never draws must not + // count as visible for the drawer's input-ownership aggregate - the app + // calls Show() at startup, so the raw flag alone would report a window on + // every plain user run. + bool IsVisible() const override; protected: void OnDraw(ImGuiIO& io) override; diff --git a/thirdparty/rexglue-sdk/include/rex/input/input_driver.h b/thirdparty/rexglue-sdk/include/rex/input/input_driver.h index b579ef4c..0e2afb3c 100644 --- a/thirdparty/rexglue-sdk/include/rex/input/input_driver.h +++ b/thirdparty/rexglue-sdk/include/rex/input/input_driver.h @@ -40,6 +40,13 @@ class InputDriver { virtual void OnWindowAvailable(rex::ui::Window* /*window*/) {} + // Whether this driver synthesizes its state from the host keyboard/mouse + // and must therefore pause while the host UI (overlay dialogs) captures + // them. Real controller drivers keep the default false: a physical pad has + // nothing to do with UI capture - the overlays are not pad-navigable - and + // must keep working while a dialog has the cursor or keyboard focus. + virtual bool SuppressedByUICapture() const { return false; } + void set_is_active_callback(std::function is_active_callback) { is_active_callback_ = is_active_callback; } diff --git a/thirdparty/rexglue-sdk/include/rex/input/mnk/mnk_input_driver.h b/thirdparty/rexglue-sdk/include/rex/input/mnk/mnk_input_driver.h index 2483b99a..b4b48c06 100644 --- a/thirdparty/rexglue-sdk/include/rex/input/mnk/mnk_input_driver.h +++ b/thirdparty/rexglue-sdk/include/rex/input/mnk/mnk_input_driver.h @@ -38,6 +38,10 @@ class MnkInputDriver final : public InputDriver, void OnWindowAvailable(rex::ui::Window* window) override; + // Synthesizes a pad from the host keyboard/mouse, so it pauses while a UI + // dialog captures them (unlike real controller drivers). + bool SuppressedByUICapture() const override { return true; } + // WindowInputListener void OnKeyDown(rex::ui::KeyEvent& e) override; void OnKeyUp(rex::ui::KeyEvent& e) override; diff --git a/thirdparty/rexglue-sdk/include/rex/ui/imgui_dialog.h b/thirdparty/rexglue-sdk/include/rex/ui/imgui_dialog.h index 1018d5e8..bc1bbe3e 100644 --- a/thirdparty/rexglue-sdk/include/rex/ui/imgui_dialog.h +++ b/thirdparty/rexglue-sdk/include/rex/ui/imgui_dialog.h @@ -39,6 +39,13 @@ class ImGuiDialog { void Draw(); + // Whether the dialog currently draws a window. Toggleable overlays override + // this with their own visibility state; a dialog without one (e.g. a message + // box) is visible for as long as it is attached. ImGuiDrawer aggregates this + // every frame to publish whether any dialog window is on screen and may own + // the mouse or keyboard (see ImGuiDrawer::DialogsCaptureMouse()). + virtual bool IsVisible() const { return true; } + protected: ImGuiDialog(ImGuiDrawer* imgui_drawer); diff --git a/thirdparty/rexglue-sdk/include/rex/ui/imgui_drawer.h b/thirdparty/rexglue-sdk/include/rex/ui/imgui_drawer.h index c72b6283..d012260c 100644 --- a/thirdparty/rexglue-sdk/include/rex/ui/imgui_drawer.h +++ b/thirdparty/rexglue-sdk/include/rex/ui/imgui_drawer.h @@ -12,6 +12,7 @@ #ifndef REX_UI_IMGUI_DRAWER_H_ #define REX_UI_IMGUI_DRAWER_H_ +#include #include #include #include @@ -58,6 +59,25 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { void Draw(UIDrawContext& ui_draw_context) override; + // Dialog input ownership, published once per frame by Draw() and cleared + // when the drawer detaches. Desktop window-manager semantics: the mouse + // belongs to the dialog window under the cursor (or that a drag started + // on), the keyboard to the dialog window holding FOCUS (freshly opened or + // clicked into; released by clicking the game area) or actively engaging a + // widget. Each flag is conjoined with "some dialog window is visible", so + // a hidden dialog can never own input regardless of any focus state ImGui + // retains for it. Safe to read from any thread, including low-level hook + // threads - a plain atomic load, no ImGui access. + static bool DialogsCaptureMouse() { + return dialogs_capture_mouse_.load(std::memory_order_relaxed); + } + static bool DialogsCaptureKeyboard() { + return dialogs_capture_keyboard_.load(std::memory_order_relaxed); + } + static bool DialogsWantTextInput() { + return dialogs_want_text_input_.load(std::memory_order_relaxed); + } + protected: void OnKeyDown(KeyEvent& e) override; void OnKeyUp(KeyEvent& e) override; @@ -84,6 +104,8 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { bool IsDrawingDialogs() const { return dialog_loop_next_index_ != SIZE_MAX; } void DetachIfLastDialogRemoved(); + void PublishDialogInputOwnership(bool capture_mouse, bool capture_keyboard, bool want_text_input); + std::optional VirtualKeyToImGuiKey(VirtualKey vkey); Window* window_; @@ -119,6 +141,11 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { double frame_time_tick_frequency_; uint64_t last_frame_time_ticks_; + + // See DialogsCaptureMouse()/DialogsCaptureKeyboard()/DialogsWantTextInput(). + static std::atomic dialogs_capture_mouse_; + static std::atomic dialogs_capture_keyboard_; + static std::atomic dialogs_want_text_input_; }; } // namespace ui diff --git a/thirdparty/rexglue-sdk/include/rex/ui/overlay/build_stamp_overlay.h b/thirdparty/rexglue-sdk/include/rex/ui/overlay/build_stamp_overlay.h index 3fad9b77..ec7b8b3e 100644 --- a/thirdparty/rexglue-sdk/include/rex/ui/overlay/build_stamp_overlay.h +++ b/thirdparty/rexglue-sdk/include/rex/ui/overlay/build_stamp_overlay.h @@ -19,6 +19,13 @@ class BuildStampOverlay : public ImGuiDialog { public: explicit BuildStampOverlay(ImGuiDrawer* imgui_drawer); + // The watermark is drawn with NoInputs|NoNav: ImGui can neither hover nor + // focus it, so it can never own the mouse or keyboard. It is also always + // attached - reporting it visible would keep the drawer's dialog input + // ownership permanently live and void the closed-overlay guarantee the + // real overlay windows rely on. It is a stamp, not a window. + bool IsVisible() const override { return false; } + protected: void OnDraw(ImGuiIO& io) override; }; diff --git a/thirdparty/rexglue-sdk/include/rex/ui/overlay/console_overlay.h b/thirdparty/rexglue-sdk/include/rex/ui/overlay/console_overlay.h index 68585080..a2852841 100644 --- a/thirdparty/rexglue-sdk/include/rex/ui/overlay/console_overlay.h +++ b/thirdparty/rexglue-sdk/include/rex/ui/overlay/console_overlay.h @@ -28,7 +28,7 @@ class ConsoleDialog : public ImGuiDialog { ~ConsoleDialog(); void ToggleVisible(); - bool IsVisible() const { return visible_; } + bool IsVisible() const override { return visible_; } protected: void OnDraw(ImGuiIO& io) override; diff --git a/thirdparty/rexglue-sdk/include/rex/ui/overlay/debug_overlay.h b/thirdparty/rexglue-sdk/include/rex/ui/overlay/debug_overlay.h index 86de1a88..d40d545c 100644 --- a/thirdparty/rexglue-sdk/include/rex/ui/overlay/debug_overlay.h +++ b/thirdparty/rexglue-sdk/include/rex/ui/overlay/debug_overlay.h @@ -30,7 +30,7 @@ class DebugOverlayDialog : public ImGuiDialog { ~DebugOverlayDialog(); void ToggleVisible() { visible_ = !visible_; } - bool IsVisible() const { return visible_; } + bool IsVisible() const override { return visible_; } void SetStatsProvider(FrameStatsProvider provider) { stats_provider_ = std::move(provider); } protected: diff --git a/thirdparty/rexglue-sdk/include/rex/ui/overlay/settings_overlay.h b/thirdparty/rexglue-sdk/include/rex/ui/overlay/settings_overlay.h index fd656c72..922ae9d2 100644 --- a/thirdparty/rexglue-sdk/include/rex/ui/overlay/settings_overlay.h +++ b/thirdparty/rexglue-sdk/include/rex/ui/overlay/settings_overlay.h @@ -23,7 +23,7 @@ class SettingsDialog : public ImGuiDialog { ~SettingsDialog(); void ToggleVisible() { visible_ = !visible_; } - bool IsVisible() const { return visible_; } + bool IsVisible() const override { return visible_; } protected: void OnDraw(ImGuiIO& io) override; diff --git a/thirdparty/rexglue-sdk/src/input/input_system.cpp b/thirdparty/rexglue-sdk/src/input/input_system.cpp index e132acb0..bf180948 100644 --- a/thirdparty/rexglue-sdk/src/input/input_system.cpp +++ b/thirdparty/rexglue-sdk/src/input/input_system.cpp @@ -52,8 +52,17 @@ void InputSystem::AttachWindow(rex::ui::Window* window) { } void InputSystem::SetActiveCallback(std::function callback) { + // The callback reports "the host UI is not capturing input". Only drivers + // that synthesize their state from the host keyboard/mouse (MnK) receive + // it; real controller drivers keep a null callback (always active), so a + // physical pad never goes dead while an overlay dialog has the cursor or + // focus. (The SDL driver zeroes its gamepad state while inactive - wiring + // the callback onto it makes a real pad drop out whenever the mouse + // hovers a dialog.) for (auto& driver : drivers_) { - driver->set_is_active_callback(callback); + if (driver->SuppressedByUICapture()) { + driver->set_is_active_callback(callback); + } } } diff --git a/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp b/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp index cbfe4d5a..17aacb3b 100644 --- a/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp +++ b/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp @@ -234,11 +234,16 @@ bool ReXApp::OnInitialize() { runtime_->set_display_window(window_.get()); runtime_->set_imgui_drawer(imgui_drawer_.get()); - // Tell input drivers to suppress input when ImGui wants the mouse - // (e.g. overlay is open). This controls MnK mouse capture. + // Pause the MnK virtual pad while a visible dialog owns the mouse. + // Uses the drawer's published per-frame flag: visibility-conjoined + // (a closed dialog can never latch it) and safe to read from the + // input threads, unlike ImGui IO. Real controller drivers never + // receive this callback (see InputSystem::SetActiveCallback) - a + // physical pad keeps working with a dialog open. auto* input_sys = static_cast(runtime_->input_system()); if (input_sys) { - input_sys->SetActiveCallback([]() { return !ImGui::GetIO().WantCaptureMouse; }); + input_sys->SetActiveCallback( + []() { return !rex::ui::ImGuiDrawer::DialogsCaptureMouse(); }); } } } diff --git a/thirdparty/rexglue-sdk/src/ui/imgui_drawer.cpp b/thirdparty/rexglue-sdk/src/ui/imgui_drawer.cpp index d9364b55..7fd56bf2 100644 --- a/thirdparty/rexglue-sdk/src/ui/imgui_drawer.cpp +++ b/thirdparty/rexglue-sdk/src/ui/imgui_drawer.cpp @@ -34,6 +34,17 @@ const char kProggyTinyCompressedDataBase85[10950 + 1] = static_assert(sizeof(ImmediateVertex) == sizeof(ImDrawVert), "Vertex types must match"); +std::atomic ImGuiDrawer::dialogs_capture_mouse_{false}; +std::atomic ImGuiDrawer::dialogs_capture_keyboard_{false}; +std::atomic ImGuiDrawer::dialogs_want_text_input_{false}; + +void ImGuiDrawer::PublishDialogInputOwnership(bool capture_mouse, bool capture_keyboard, + bool want_text_input) { + dialogs_capture_mouse_.store(capture_mouse, std::memory_order_relaxed); + dialogs_capture_keyboard_.store(capture_keyboard, std::memory_order_relaxed); + dialogs_want_text_input_.store(want_text_input, std::memory_order_relaxed); +} + ImGuiDrawer::ImGuiDrawer(rex::ui::Window* window, size_t z_order, FontSetupCallback font_setup) : window_(window), z_order_(z_order), font_setup_(std::move(font_setup)) { Initialize(); @@ -359,10 +370,12 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) { if (!immediate_drawer_) { // A presenter has been attached, but an immediate drawer hasn't been // attached yet. + PublishDialogInputOwnership(false, false, false); return; } if (dialogs_.empty()) { + PublishDialogInputOwnership(false, false, false); return; } @@ -393,6 +406,29 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) { } dialog_loop_next_index_ = SIZE_MAX; + // Publish this frame's dialog input ownership: the ImGui capture flags + // (computed in NewFrame) conjoined with post-loop visibility, so a dialog + // hidden this frame (e.g. via its toggle hotkey) stops owning input + // immediately, whatever focus state ImGui still holds for its window. + bool any_dialog_visible = false; + for (ImGuiDialog* dialog : dialogs_) { + if (dialog->IsVisible()) { + any_dialog_visible = true; + break; + } + } + // Keyboard follows FOCUS, and in this ImGui version io.WantCaptureKeyboard + // alone is not focus - it is only true while a widget is actively engaged + // (InputText active, slider held, modal open). A focused-but-idle dialog + // window must still own the keyboard, like any focused desktop window, so + // OR in "some ImGui window holds focus" (a freshly opened dialog takes + // focus; clicking the game void releases it). + const bool keyboard_owned = + io.WantCaptureKeyboard || ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow); + PublishDialogInputOwnership(any_dialog_visible && io.WantCaptureMouse, + any_dialog_visible && keyboard_owned, + any_dialog_visible && io.WantTextInput); + ImGui::Render(); ImDrawData* draw_data = ImGui::GetDrawData(); if (draw_data) { @@ -647,6 +683,8 @@ void ImGuiDrawer::DetachIfLastDialogRemoved() { // which will be persistent until new events actualize individual input // properties. ClearInput(); + // No dialogs left: nothing can own input until the drawer reattaches. + PublishDialogInputOwnership(false, false, false); } } // namespace ui diff --git a/thirdparty/rexglue-sdk/src/ui/overlay/settings_overlay.cpp b/thirdparty/rexglue-sdk/src/ui/overlay/settings_overlay.cpp index 85b38218..6cd9fea9 100644 --- a/thirdparty/rexglue-sdk/src/ui/overlay/settings_overlay.cpp +++ b/thirdparty/rexglue-sdk/src/ui/overlay/settings_overlay.cpp @@ -344,6 +344,11 @@ void SettingsDialog::OnDraw(ImGuiIO& /*io*/) { bool is_capturing = (capturing_bind_name_ == entry.name); if (is_capturing) { + // Waiting for the new binding: the next key press belongs to this + // window, not to the application. No widget is active while waiting + // (the Rebind button was already released), so WantCaptureKeyboard + // would read false - declare keyboard ownership explicitly. + ImGui::SetNextFrameWantCaptureKeyboard(true); ImGui::Button("Press any key...##v", ImVec2(140.0f, 0)); if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {