From 4ea96330586e63e477e61fe2eb8b84c3f0933df9 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 9 Aug 2026 00:09:54 +0200 Subject: [PATCH 1/3] display: hide the SDL host pointer when the guest draws its own cursor When the guest OS draws a hardware cursor into the framebuffer, the SDL host pointer was still shown on top, so the user saw two cursors that did not track each other. Hide the host pointer while a guest cursor is drawn and show it again otherwise, so the user can aim the mouse at the window when no guest cursor is present. --- devices/video/display_sdl.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/devices/video/display_sdl.cpp b/devices/video/display_sdl.cpp index 9262cb8ca9..d51bd55096 100644 --- a/devices/video/display_sdl.cpp +++ b/devices/video/display_sdl.cpp @@ -49,6 +49,7 @@ class Display::Impl { SDL_Texture* disp_texture = 0; SDL_Texture* cursor_texture = 0; SDL_Rect cursor_rect; // destination rectangle for cursor drawing + bool show_host_cursor = true; // desired SDL host pointer visibility int display_w; int display_h; double drawable_w; @@ -466,6 +467,16 @@ void Display::update(std::function conver SDL_RenderClear(impl->renderer); SDL_RenderCopy(impl->renderer, impl->disp_texture, NULL, &impl->dest_rect); + // The guest might draw its own cursor into the framebuffer. In that case + // hide the SDL host pointer so the user doesn't see two cursors that don't + // track each other. Otherwise show the host pointer so the user can still + // aim the mouse at the window before grabbing it. + bool want_host_cursor = !draw_hw_cursor; + if (impl->show_host_cursor != want_host_cursor) { + impl->show_host_cursor = want_host_cursor; + SDL_ShowCursor(want_host_cursor ? SDL_ENABLE : SDL_DISABLE); + } + // draw HW cursor if enabled if (draw_hw_cursor) { impl->cursor_rect.x = cursor_x * impl->renderer_scale_x + impl->dest_rect.x; From 878ea0ce7796536092ebfcbc4c17c252b8364330 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 9 Aug 2026 00:10:24 +0200 Subject: [PATCH 2/3] display: auto-grab the mouse when the guest draws its own cursor While a guest cursor is drawn, the host mouse is now grabbed automatically so the guest cursor can reach every screen area and the host pointer cannot wander off the window. The grab is released again when the guest cursor disappears, and re-established when the window regains focus, because SDL may drop the grab on focus loss. A manual grab with Ctrl+G is sticky and overrides the automatic behavior. --- devices/video/display_sdl.cpp | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/devices/video/display_sdl.cpp b/devices/video/display_sdl.cpp index d51bd55096..cfcd874778 100644 --- a/devices/video/display_sdl.cpp +++ b/devices/video/display_sdl.cpp @@ -50,6 +50,8 @@ class Display::Impl { SDL_Texture* cursor_texture = 0; SDL_Rect cursor_rect; // destination rectangle for cursor drawing bool show_host_cursor = true; // desired SDL host pointer visibility + bool guest_cursor_drawn = false; // guest is drawing its own cursor + bool manual_grab = false; // grab toggled on with Ctrl+G int display_w; int display_h; double drawable_w; @@ -313,6 +315,13 @@ void Display::handle_events(const WindowEvent& wnd_event) { if (wnd_event.window_id == impl->disp_wnd_id) { SDL_SetHint(SDL_HINT_ALLOW_ALT_TAB_WHILE_GRABBED, "0"); SDL_SetWindowKeyboardGrab(impl->display_wnd, SDL_TRUE); + // When the window (re)gains focus (e.g. the user clicks it), SDL + // may have dropped the relative mouse grab on focus loss. Re-grab + // it so the guest cursor keeps working; this is the reliable + // re-grab trigger instead of polling every frame. + if (impl->guest_cursor_drawn && !impl->manual_grab && !SDL_GetRelativeMouseMode()) { + SDL_SetRelativeMouseMode(SDL_TRUE); + } } break; @@ -395,9 +404,11 @@ void Display::toggle_mouse_grab() { if (SDL_GetRelativeMouseMode()) { SDL_SetRelativeMouseMode(SDL_FALSE); + impl->manual_grab = false; } else { this->update_mouse_grab(true); SDL_SetRelativeMouseMode(SDL_TRUE); + impl->manual_grab = true; } } @@ -467,16 +478,40 @@ void Display::update(std::function conver SDL_RenderClear(impl->renderer); SDL_RenderCopy(impl->renderer, impl->disp_texture, NULL, &impl->dest_rect); - // The guest might draw its own cursor into the framebuffer. In that case - // hide the SDL host pointer so the user doesn't see two cursors that don't - // track each other. Otherwise show the host pointer so the user can still - // aim the mouse at the window before grabbing it. + bool is_grabbed = SDL_GetRelativeMouseMode(); + + // The guest draws its own cursor into the framebuffer (hardware cursor). + // When such a cursor session starts, auto-grab the host mouse so the + // pointer cannot leave the window and the guest cursor can reach every + // screen area. When the session ends, release an automatic grab again so + // the user can move the pointer to other windows. A manual grab (Ctrl+G) + // is sticky and is neither grabbed nor released automatically. + if (draw_hw_cursor) { + if (!impl->guest_cursor_drawn && !impl->manual_grab && !is_grabbed) { + this->update_mouse_grab(true); + SDL_SetRelativeMouseMode(SDL_TRUE); + is_grabbed = true; + } + } else { + if (!impl->manual_grab && is_grabbed) { + SDL_SetRelativeMouseMode(SDL_FALSE); + is_grabbed = false; + } + } + + // The guest draws its own cursor into the framebuffer. Even without a + // grab, keep the SDL host pointer hidden so the user never sees a second + // cursor that does not track the guest one. When the guest cursor is not + // drawn (e.g. firmware), show the host pointer so the user can aim and + // click. bool want_host_cursor = !draw_hw_cursor; if (impl->show_host_cursor != want_host_cursor) { impl->show_host_cursor = want_host_cursor; SDL_ShowCursor(want_host_cursor ? SDL_ENABLE : SDL_DISABLE); } + impl->guest_cursor_drawn = draw_hw_cursor; + // draw HW cursor if enabled if (draw_hw_cursor) { impl->cursor_rect.x = cursor_x * impl->renderer_scale_x + impl->dest_rect.x; From 49b37019c96d9237b2f0c7387ba2d022b469c4ad Mon Sep 17 00:00:00 2001 From: probonopd Date: Sun, 9 Aug 2026 00:10:32 +0200 Subject: [PATCH 3/3] display: show mouse grab state and shortcut in the window title The window title now reflects whether the mouse is grabbed and how to toggle it, so the user knows how to release the automatic grab and move the pointer to other windows, or how to re-grab it. --- devices/video/display_sdl.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/devices/video/display_sdl.cpp b/devices/video/display_sdl.cpp index cfcd874778..486ca4ef1f 100644 --- a/devices/video/display_sdl.cpp +++ b/devices/video/display_sdl.cpp @@ -51,6 +51,7 @@ class Display::Impl { SDL_Rect cursor_rect; // destination rectangle for cursor drawing bool show_host_cursor = true; // desired SDL host pointer visibility bool guest_cursor_drawn = false; // guest is drawing its own cursor + bool was_grabbed = false; // last reported grab state bool manual_grab = false; // grab toggled on with Ctrl+G int display_w; int display_h; @@ -321,6 +322,7 @@ void Display::handle_events(const WindowEvent& wnd_event) { // re-grab trigger instead of polling every frame. if (impl->guest_cursor_drawn && !impl->manual_grab && !SDL_GetRelativeMouseMode()) { SDL_SetRelativeMouseMode(SDL_TRUE); + this->update_window_title(); } } break; @@ -446,7 +448,9 @@ void Display::update_window_title() std::to_string(impl->display_w) + "x" + std::to_string(impl->display_h) + " " + std::to_string(int(std::round(impl->renderer_scale_x * 100))) + "%"; if (is_grabbed) - new_window_title += " (🖱 Grabbed)"; + new_window_title += " (Grabbed, Ctrl+G to release)"; + else if (impl->guest_cursor_drawn) + new_window_title += " (Press Ctrl+G to grab)"; if (new_window_title != old_window_title) SDL_SetWindowTitle(impl->display_wnd, new_window_title.c_str()); @@ -510,7 +514,12 @@ void Display::update(std::function conver SDL_ShowCursor(want_host_cursor ? SDL_ENABLE : SDL_DISABLE); } - impl->guest_cursor_drawn = draw_hw_cursor; + // Let the window title reflect the current grab/cursor state. + if (is_grabbed != impl->was_grabbed || draw_hw_cursor != impl->guest_cursor_drawn) { + impl->was_grabbed = is_grabbed; + impl->guest_cursor_drawn = draw_hw_cursor; + this->update_window_title(); + } // draw HW cursor if enabled if (draw_hw_cursor) {