From 42b6ab3b8527cd4929b81f8cfed1b065ae4166af Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:28:19 +0200 Subject: [PATCH] Hide the free cursor after idle (default 3 s) Hide the free cursor after idle (ac6_cursor_hide_seconds, 3 s) The free cursor now disappears after N seconds without motion and returns instantly on motion or a button. Never hides while an overlay is on screen (new DialogsVisible presence flag). Default 3 s, 0 = never, live. --- src/ac6_backend_fixes/ac6_kbm_input.cpp | 93 +++++++++++++++++++ .../rexglue-sdk/include/rex/ui/imgui_drawer.h | 11 ++- .../rexglue-sdk/src/ui/imgui_drawer.cpp | 14 +-- 3 files changed, 110 insertions(+), 8 deletions(-) diff --git a/src/ac6_backend_fixes/ac6_kbm_input.cpp b/src/ac6_backend_fixes/ac6_kbm_input.cpp index a4c71193..6a24d9fe 100644 --- a/src/ac6_backend_fixes/ac6_kbm_input.cpp +++ b/src/ac6_backend_fixes/ac6_kbm_input.cpp @@ -78,6 +78,10 @@ REXCVAR_DEFINE_STRING(ac6_kbm_config, "ac6_input.toml", "AC6/Enhancements", REXCVAR_DEFINE_BOOL(ac6_kbm_padless, true, "AC6/Enhancements", "Provide a virtual controller when none is connected, so " "keyboard and mouse work on their own."); +REXCVAR_DEFINE_DOUBLE(ac6_cursor_hide_seconds, 3.0, "AC6/Enhancements", + "Hide the free mouse cursor after this many seconds without " + "motion (0 = never hide).") + .range(0.0, 300.0); // The SDK's own mnk virtual-pad driver (input/mnk) - forced off while our // KB+M is enabled so two keyboard mappers never fight over the same pad. @@ -732,11 +736,47 @@ std::atomic g_hide_cursor{false}; WNDPROC g_orig_wndproc = nullptr; HWND g_subclassed_hwnd = nullptr; +// Free-cursor idle hide (see CursorIdleHideTick below): hidden after +// ac6_cursor_hide_seconds without motion, revealed on any real mouse input. +std::atomic g_idle_hidden{false}; +std::atomic g_idle_last_motion_ms{0}; + LRESULT CALLBACK KbmWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { if (msg == WM_SETCURSOR && g_hide_cursor.load(std::memory_order_relaxed)) { SetCursor(nullptr); return TRUE; } + if (msg == WM_MOUSEMOVE) { + // WM_MOUSEMOVE is also sent for window-management reasons with the + // cursor unmoved (see the SDK's note in window_win.cpp), so only a + // POSITION CHANGE counts as real motion. Window-thread-only state. + static LPARAM s_last_move_lp = -1; + if (lp != s_last_move_lp) { + s_last_move_lp = lp; + if (g_idle_hidden.load(std::memory_order_relaxed)) { + // Restamp the idle clock so the poll tick cannot immediately + // re-hide. A motion event's own WM_SETCURSOR is sent BEFORE its + // WM_MOUSEMOVE lands (still hidden then), so nudge a fresh one - + // the arrow returns on THIS motion, not the next. + g_idle_last_motion_ms.store(NowMs(), std::memory_order_relaxed); + g_idle_hidden.store(false, std::memory_order_relaxed); + PostMessageW(hwnd, WM_SETCURSOR, reinterpret_cast(hwnd), HTCLIENT); + } + } + } else if (msg == WM_LBUTTONDOWN || msg == WM_RBUTTONDOWN || msg == WM_MBUTTONDOWN || + msg == WM_XBUTTONDOWN || msg == WM_MOUSEWHEEL) { + if (g_idle_hidden.load(std::memory_order_relaxed)) { + g_idle_last_motion_ms.store(NowMs(), std::memory_order_relaxed); + g_idle_hidden.store(false, std::memory_order_relaxed); + PostMessageW(hwnd, WM_SETCURSOR, reinterpret_cast(hwnd), HTCLIENT); + } + } else if (msg == WM_SETCURSOR && g_idle_hidden.load(std::memory_order_relaxed) && + LOWORD(lp) == HTCLIENT) { + // Keep the cursor away over the client area while idle-hidden (window + // management re-sets it otherwise). Non-client keeps normal arrows. + SetCursor(nullptr); + return TRUE; + } return CallWindowProcW(g_orig_wndproc, hwnd, msg, wp, lp); } @@ -759,8 +799,54 @@ void SetCursorHidden(bool hide, HWND hwnd) { if (hide) SetCursor(nullptr); } } + +// ---- Free-cursor idle hide -------------------------------------------------- +// The FREE cursor (no steering capture) disappears after +// ac6_cursor_hide_seconds without motion and returns instantly on motion or +// a button - menus and desktop-style contexts; the capture hiding above is +// untouched. Never hides while an overlay is visible under the free cursor +// (a vanishing pointer over the F4 menu is an anti-feature), while the +// window is unfocused, or while capture owns the cursor. Runs on the input +// poll; the subclass proc above answers WM_SETCURSOR while hidden and +// reveals on real mouse input. Works for pad users too - not gated on +// ac6_kbm_enabled. +void CursorIdleHideTick() { + const double secs = REXCVAR_GET(ac6_cursor_hide_seconds); + const int64_t now = NowMs(); + bool want_hide = false; + if (secs > 0.0 && !g_hide_cursor.load(std::memory_order_relaxed)) { + HWND fg = GetForegroundWindow(); + DWORD pid = 0; + if (fg) GetWindowThreadProcessId(fg, &pid); + if (fg && pid == GetCurrentProcessId()) { + EnsureCursorSubclass(fg); + POINT p{}; + if (GetCursorPos(&p)) { + static POINT s_last{}; // poll-thread only + static bool s_have_last = false; + if (!s_have_last || p.x != s_last.x || p.y != s_last.y) { + s_have_last = true; + s_last = p; + g_idle_last_motion_ms.store(now, std::memory_order_relaxed); + } else if (!rex::ui::ImGuiDrawer::DialogsVisible() && + now - g_idle_last_motion_ms.load(std::memory_order_relaxed) >= + static_cast(secs * 1000.0)) { + want_hide = true; + } + } + } + } + if (g_idle_hidden.exchange(want_hide, std::memory_order_relaxed) != want_hide && + g_subclassed_hwnd) { + // Nudge the window thread to apply the new state (hide, or re-arrow + // after an overlay opened / the cvar changed - no mouse motion needed). + PostMessageW(g_subclassed_hwnd, WM_SETCURSOR, reinterpret_cast(g_subclassed_hwnd), + HTCLIENT); + } +} #else void SetCursorHidden(bool, void*) {} +void CursorIdleHideTick() {} #endif // ---- Mouse steering (M3) ----------------------------------------------------- @@ -1136,6 +1222,13 @@ PPC_FUNC_IMPL(rex_sub_82390CE0) { const uint32_t state_ptr = ctx.r4.u32; __imp__rex_sub_82390CE0(ctx, base); + // Free-cursor idle hide rides the input poll (user 0 = once per poll + // round). Deliberately outside the ac6_kbm_enabled gate: pad users get + // the timeout too. + if (user == 0) { + CursorIdleHideTick(); + } + // Pad-less operation: when no controller is connected (0x48F), present a // neutral synthetic pad on slot 0; the injection below then supplies the // keyboard/mouse state exactly as if a pad were plugged in. diff --git a/thirdparty/rexglue-sdk/include/rex/ui/imgui_drawer.h b/thirdparty/rexglue-sdk/include/rex/ui/imgui_drawer.h index d012260c..40c59fc0 100644 --- a/thirdparty/rexglue-sdk/include/rex/ui/imgui_drawer.h +++ b/thirdparty/rexglue-sdk/include/rex/ui/imgui_drawer.h @@ -77,6 +77,10 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { static bool DialogsWantTextInput() { return dialogs_want_text_input_.load(std::memory_order_relaxed); } + // Whether any dialog window is visible at all, independent of input + // ownership - for policies that key off overlay PRESENCE (e.g. never + // auto-hiding the cursor while an overlay is on screen), not hover/focus. + static bool DialogsVisible() { return dialogs_visible_.load(std::memory_order_relaxed); } protected: void OnKeyDown(KeyEvent& e) override; @@ -104,7 +108,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); + void PublishDialogInputOwnership(bool any_visible, bool capture_mouse, bool capture_keyboard, + bool want_text_input); std::optional VirtualKeyToImGuiKey(VirtualKey vkey); @@ -142,10 +147,12 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer { double frame_time_tick_frequency_; uint64_t last_frame_time_ticks_; - // See DialogsCaptureMouse()/DialogsCaptureKeyboard()/DialogsWantTextInput(). + // See DialogsCaptureMouse()/DialogsCaptureKeyboard()/DialogsWantTextInput()/ + // DialogsVisible(). static std::atomic dialogs_capture_mouse_; static std::atomic dialogs_capture_keyboard_; static std::atomic dialogs_want_text_input_; + static std::atomic dialogs_visible_; }; } // namespace ui diff --git a/thirdparty/rexglue-sdk/src/ui/imgui_drawer.cpp b/thirdparty/rexglue-sdk/src/ui/imgui_drawer.cpp index 7fd56bf2..564ab3d9 100644 --- a/thirdparty/rexglue-sdk/src/ui/imgui_drawer.cpp +++ b/thirdparty/rexglue-sdk/src/ui/imgui_drawer.cpp @@ -37,9 +37,11 @@ static_assert(sizeof(ImmediateVertex) == sizeof(ImDrawVert), "Vertex types must std::atomic ImGuiDrawer::dialogs_capture_mouse_{false}; std::atomic ImGuiDrawer::dialogs_capture_keyboard_{false}; std::atomic ImGuiDrawer::dialogs_want_text_input_{false}; +std::atomic ImGuiDrawer::dialogs_visible_{false}; -void ImGuiDrawer::PublishDialogInputOwnership(bool capture_mouse, bool capture_keyboard, - bool want_text_input) { +void ImGuiDrawer::PublishDialogInputOwnership(bool any_visible, bool capture_mouse, + bool capture_keyboard, bool want_text_input) { + dialogs_visible_.store(any_visible, std::memory_order_relaxed); 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); @@ -370,12 +372,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); + PublishDialogInputOwnership(false, false, false, false); return; } if (dialogs_.empty()) { - PublishDialogInputOwnership(false, false, false); + PublishDialogInputOwnership(false, false, false, false); return; } @@ -425,7 +427,7 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) { // focus; clicking the game void releases it). const bool keyboard_owned = io.WantCaptureKeyboard || ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow); - PublishDialogInputOwnership(any_dialog_visible && io.WantCaptureMouse, + PublishDialogInputOwnership(any_dialog_visible, any_dialog_visible && io.WantCaptureMouse, any_dialog_visible && keyboard_owned, any_dialog_visible && io.WantTextInput); @@ -684,7 +686,7 @@ void ImGuiDrawer::DetachIfLastDialogRemoved() { // properties. ClearInput(); // No dialogs left: nothing can own input until the drawer reattaches. - PublishDialogInputOwnership(false, false, false); + PublishDialogInputOwnership(false, false, false, false); } } // namespace ui