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.
This commit is contained in:
Dipshet
2026-08-07 23:28:19 +02:00
parent 4ca74fd107
commit 42b6ab3b85
3 changed files with 110 additions and 8 deletions
+93
View File
@@ -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<bool> 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<bool> g_idle_hidden{false};
std::atomic<int64_t> 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<WPARAM>(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<WPARAM>(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<int64_t>(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<WPARAM>(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.
+9 -2
View File
@@ -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<ImGuiKey> 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<bool> dialogs_capture_mouse_;
static std::atomic<bool> dialogs_capture_keyboard_;
static std::atomic<bool> dialogs_want_text_input_;
static std::atomic<bool> dialogs_visible_;
};
} // namespace ui
+8 -6
View File
@@ -37,9 +37,11 @@ static_assert(sizeof(ImmediateVertex) == sizeof(ImDrawVert), "Vertex types must
std::atomic<bool> ImGuiDrawer::dialogs_capture_mouse_{false};
std::atomic<bool> ImGuiDrawer::dialogs_capture_keyboard_{false};
std::atomic<bool> ImGuiDrawer::dialogs_want_text_input_{false};
std::atomic<bool> 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