From feeced81d2be8c50f4b64b2ca3b6f216dad6fb07 Mon Sep 17 00:00:00 2001 From: Aryan Sharma <81407258+Aryansh1520@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:45:15 +0530 Subject: [PATCH] window: auto-hide the cursor over the game window (#958) * window: auto-hide the cursor over the game window Hide the cursor when the game window is created and whenever the mouse has been idle for two seconds; any mouse motion, button or wheel event shows it again. Games that draw their own cursor (e.g. Blue Prince) no longer show the host pointer on top of it. Only real mouse input counts as activity: synthetic touch/pen mouse events and zero-delta motion (sent by some platforms on focus or cursor visibility changes) are ignored. The event loop only waits with a timeout while the cursor is visible, so it still blocks indefinitely once the cursor is hidden. The cursor is restored on shutdown. Uses SDL_HideCursor/SDL_ShowCursor, which SDL implements for X11, Wayland, Cocoa and Win32 and which only affect the cursor over the emulator window. Closes #282 * window: make cursor auto-hide optional in CLI and launcher Co-authored-by: Anatolii Kucheruk --------- Co-authored-by: nmzik Co-authored-by: Anatolii Kucheruk --- src/common/emulatorConfig.cpp | 4 ++ src/common/emulatorConfig.h | 2 + .../presentation/window/hostInput.cpp | 58 ++++++++++++++++--- .../forms/configuration_edit_dialog.ui | 6 ++ src/launcher/include/configuration.h | 4 ++ src/launcher/src/configurationEditDialog.cpp | 2 + src/launcher/src/mainDialog.cpp | 3 + src/main.cpp | 7 +++ 8 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/common/emulatorConfig.cpp b/src/common/emulatorConfig.cpp index 5d421df97..ccceb5f8c 100644 --- a/src/common/emulatorConfig.cpp +++ b/src/common/emulatorConfig.cpp @@ -72,6 +72,10 @@ bool FullscreenEnabled() { return g_config->fullscreen_enabled; } +bool HideCursorEnabled() { + return g_config->hide_cursor_enabled; +} + bool VrEnabled() { return g_config->vr_enabled; } diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index 7ace3c175..58a9e24d2 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -53,6 +53,7 @@ struct ConfigOptions { PresentMode present_mode = PresentMode::Mailbox; int32_t gpu_index = -1; bool fullscreen_enabled = false; + bool hide_cursor_enabled = false; bool vr_enabled = false; bool amd_cpu_enabled = false; uint32_t vblank_frequency = 60; @@ -93,6 +94,7 @@ uint32_t GetControllerVibrationIntensity(); PresentMode GetPresentMode(); int32_t GetGpuIndex(); bool FullscreenEnabled(); +bool HideCursorEnabled(); bool VrEnabled(); bool AmdCpuEnabled(); uint32_t GetVblankFrequency(); diff --git a/src/graphics/presentation/window/hostInput.cpp b/src/graphics/presentation/window/hostInput.cpp index 425e01686..d04d74fbf 100644 --- a/src/graphics/presentation/window/hostInput.cpp +++ b/src/graphics/presentation/window/hostInput.cpp @@ -72,6 +72,7 @@ struct Binding { }; constexpr int MOUSE_POLL_INTERVAL_MS = 33; +constexpr uint64_t CURSOR_IDLE_HIDE_MS = 2000; constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity="; struct MouseJoystickState { @@ -81,7 +82,8 @@ struct MouseJoystickState { }; MouseJoystickState g_mouse; -SDL_Window* g_mouse_window = nullptr; +SDL_Window* g_mouse_window = nullptr; +uint64_t g_cursor_hide_at = 0; std::size_t ControlFromName(std::string_view name) { const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(), @@ -380,11 +382,31 @@ int PollMouse(uint64_t now_ms) { return MOUSE_POLL_INTERVAL_MS; } +bool IsCursorActivity(const SDL_Event& event) { + SDL_MouseID which; + switch (event.type) { + case SDL_EVENT_MOUSE_MOTION: + if (event.motion.xrel == 0.0f && event.motion.yrel == 0.0f) { + return false; + } + which = event.motion.which; + break; + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: which = event.button.which; break; + case SDL_EVENT_MOUSE_WHEEL: which = event.wheel.which; break; + default: return false; + } + return which != SDL_TOUCH_MOUSEID && which != SDL_PEN_MOUSEID; +} + } // namespace void HostInputInit(SDL_Window* window) { GetInputMap(); g_mouse_window = window; + if (Config::HideCursorEnabled()) { + g_cursor_hide_at = SDL_GetTicks() + CURSOR_IDLE_HIDE_MS; + } } void HostInputShutdown() { @@ -393,7 +415,11 @@ void HostInputShutdown() { CenterMouseStick(); g_mouse = {}; } - g_mouse_window = nullptr; + g_mouse_window = nullptr; + g_cursor_hide_at = 0; + if (Config::HideCursorEnabled()) { + SDL_ShowCursor(); + } } void HostInputKey(int key_code, bool down) { @@ -429,20 +455,36 @@ void HostInputToggleMouseToJoystick() { } bool HostInputWaitEvent(SDL_Event* event) { - bool has_event; + int timeout = -1; if (!g_mouse.enabled || SDL_GetKeyboardFocus() != g_mouse_window) { g_mouse.next_poll = 0; CenterMouseStick(); - has_event = SDL_WaitEvent(event); - if (!has_event) { - EXIT("%s\n", SDL_GetError()); - } } else { if (g_mouse.next_poll == 0) { SDL_GetRelativeMouseState(nullptr, nullptr); g_mouse.next_poll = SDL_GetTicks() + MOUSE_POLL_INTERVAL_MS; } - has_event = SDL_WaitEventTimeout(event, PollMouse(SDL_GetTicks())); + timeout = PollMouse(SDL_GetTicks()); + } + + if (g_cursor_hide_at != 0) { + const auto now_ms = SDL_GetTicks(); + const int cursor_timeout = + now_ms < g_cursor_hide_at ? static_cast(g_cursor_hide_at - now_ms) : 0; + timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout); + } + const bool has_event = SDL_WaitEventTimeout(event, timeout); + + if (Config::HideCursorEnabled()) { + const auto now_ms = SDL_GetTicks(); + if (has_event && !g_mouse.enabled && IsCursorActivity(*event) && + SDL_GetWindowFromEvent(event) == g_mouse_window) { + SDL_ShowCursor(); + g_cursor_hide_at = now_ms + CURSOR_IDLE_HIDE_MS; + } else if (g_cursor_hide_at != 0 && now_ms >= g_cursor_hide_at) { + SDL_HideCursor(); + g_cursor_hide_at = 0; + } } if (has_event && event->type == SDL_EVENT_WINDOW_FOCUS_LOST && diff --git a/src/launcher/forms/configuration_edit_dialog.ui b/src/launcher/forms/configuration_edit_dialog.ui index 05daec599..2ac240c5c 100644 --- a/src/launcher/forms/configuration_edit_dialog.ui +++ b/src/launcher/forms/configuration_edit_dialog.ui @@ -173,6 +173,12 @@ Enable tessellation support (experimental) + + + Hide the host cursor after 2 seconds without mouse activity + Auto-hide cursor + + diff --git a/src/launcher/include/configuration.h b/src/launcher/include/configuration.h index 638f7ed5d..c5e01351b 100644 --- a/src/launcher/include/configuration.h +++ b/src/launcher/include/configuration.h @@ -119,6 +119,7 @@ public: PresentMode present_mode = PresentMode::Mailbox; int gpu_index = -1; bool fullscreen_enabled = false; + bool hide_cursor_enabled = false; bool readback_linear_images = false; bool tessellation_enabled = false; int vblank_frequency = 60; @@ -150,6 +151,7 @@ public: present_mode = other.present_mode; gpu_index = other.gpu_index; fullscreen_enabled = other.fullscreen_enabled; + hide_cursor_enabled = other.hide_cursor_enabled; readback_linear_images = other.readback_linear_images; tessellation_enabled = other.tessellation_enabled; vblank_frequency = other.vblank_frequency; @@ -196,6 +198,7 @@ public: KYTY_CFG_SET(present_mode); KYTY_CFG_SET(gpu_index); KYTY_CFG_SET(fullscreen_enabled); + KYTY_CFG_SET(hide_cursor_enabled); KYTY_CFG_SET(readback_linear_images); KYTY_CFG_SET(tessellation_enabled); KYTY_CFG_SET(vblank_frequency); @@ -238,6 +241,7 @@ public: present_mode = PresentMode::Mailbox; } KYTY_CFG_GET(fullscreen_enabled); + KYTY_CFG_GET(hide_cursor_enabled); KYTY_CFG_GET(readback_linear_images); KYTY_CFG_GET(tessellation_enabled); vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt(); diff --git a/src/launcher/src/configurationEditDialog.cpp b/src/launcher/src/configurationEditDialog.cpp index 5e1c173df..682be2a91 100644 --- a/src/launcher/src/configurationEditDialog.cpp +++ b/src/launcher/src/configurationEditDialog.cpp @@ -273,6 +273,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) { info.gpu_index >= 0 && info.gpu_index < m_ui->comboBox_gpu->count() - 1 ? info.gpu_index + 1 : 0); m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled); + m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled); m_ui->checkBox_readback->setChecked(info.readback_linear_images); m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled); m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency); @@ -425,6 +426,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo TextToEnum(ui.comboBox_present_mode->currentText()); info.gpu_index = ui.comboBox_gpu->currentIndex() - 1; info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked(); + info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked(); info.readback_linear_images = ui.checkBox_readback->isChecked(); info.tessellation_enabled = ui.checkBox_tessellation->isChecked(); info.vblank_frequency = ui.spinBox_vblank_frequency->value(); diff --git a/src/launcher/src/mainDialog.cpp b/src/launcher/src/mainDialog.cpp index 81d404004..43792d4fe 100644 --- a/src/launcher/src/mainDialog.cpp +++ b/src/launcher/src/mainDialog.cpp @@ -240,6 +240,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) { if (info.fullscreen_enabled) { args << "--fullscreen"; } + if (info.hide_cursor_enabled) { + args << "--hide-cursor"; + } args << "--readback-linear-images" << BoolArg(info.readback_linear_images); if (info.tessellation_enabled) { args << "--tessellation"; diff --git a/src/main.cpp b/src/main.cpp index be1607bf5..f821e931d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -61,6 +61,8 @@ static void PrintUsage() { ::printf( " --gpu Vulkan physical device index. Default: auto.\n"); ::printf(" --fullscreen Run in borderless desktop fullscreen.\n"); + ::printf( + " --hide-cursor Hide the cursor after 2 s idle. Default: off.\n"); ::printf(" --vr Enable the virtual VR headset.\n"); ::printf(" --amd-cpu Apply AMD CPU instruction patches.\n"); ::printf(" --vblank-frequency Virtual vblank frequency. Default: 60.\n"); @@ -205,6 +207,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he continue; } + if (arg == "--hide-cursor") { + options.config.hide_cursor_enabled = true; + continue; + } + if (arg == "--vr") { options.config.vr_enabled = true; continue;