From 1cc1a445e31ca94e2723788de03e121b59d9f8de Mon Sep 17 00:00:00 2001 From: David Thiel Date: Mon, 8 Jun 2026 14:46:40 +0100 Subject: [PATCH] Fix focus_follows_mouse switching active window on desktop/space return When focus_follows_mouse is enabled, returning to a desktop/space fired an enter event that switched the active window to whichever one was under the cursor, even though the mouse had not crossed a window boundary. Distinguish genuine mouse motion into a window from a window reappearing under a stationary cursor by checking, in cursor_enter_callback, whether the cursor position actually changed. focus_follows_mouse now switches focus only when the cursor moved, so motion into a window still switches focus while a stationary reappearance does not. --- docs/changelog.rst | 2 ++ kitty/data-types.h | 2 +- kitty/glfw.c | 14 +++++++++++--- kitty/mouse.c | 16 ++++++++-------- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d818deeb8..463744801 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -178,6 +178,8 @@ Detailed list of changes - macOS: Show a key symbol on the active tab if the macOS Secure Input feature is enabled +- Fix :opt:`focus_follows_mouse` switching the active window when returning to a desktop/space, even though the mouse did not move. Now the window under a stationary cursor is left alone, while moving the mouse across windows still switches focus as before. + 0.47.2 [2026-06-07] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/data-types.h b/kitty/data-types.h index d7fb92aa3..e8af3d011 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -329,7 +329,7 @@ bool colorprofile_pop_colors(ColorProfile*, unsigned int); void colorprofile_report_stack(ColorProfile*, unsigned int*, unsigned int*); void set_mouse_cursor(MouseShape); -void enter_event(int modifiers); +void enter_event(int modifiers, bool cursor_moved); void leave_event(int modifiers); void mouse_event(const int, int, int); void focus_in_event(void); diff --git a/kitty/glfw.c b/kitty/glfw.c index c32a18d73..655f6d878 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -539,12 +539,20 @@ cursor_enter_callback(GLFWwindow *w, int entered) { glfwGetCursorPos(w, &x, &y); monotonic_t now = monotonic(); global_state.callback_os_window->last_mouse_activity_at = now; - global_state.callback_os_window->mouse_x = x * global_state.callback_os_window->viewport_x_ratio; - global_state.callback_os_window->mouse_y = y * global_state.callback_os_window->viewport_y_ratio; + double new_mouse_x = x * global_state.callback_os_window->viewport_x_ratio; + double new_mouse_y = y * global_state.callback_os_window->viewport_y_ratio; + // focus_follows_mouse should react to the mouse moving, not to a window + // appearing under a stationary cursor (such as when returning to this + // desktop/space). Detect genuine motion by comparing against the last + // known cursor position so an enter caused by mouse motion still switches + // focus, while a stationary reappearance does not. + bool cursor_moved = new_mouse_x != global_state.callback_os_window->mouse_x || new_mouse_y != global_state.callback_os_window->mouse_y; + global_state.callback_os_window->mouse_x = new_mouse_x; + global_state.callback_os_window->mouse_y = new_mouse_y; if (entered) { debug_input("Mouse cursor entered window: %llu at %fx%f\n", global_state.callback_os_window->id, x, y); cursor_active_callback(now); - if (is_window_ready_for_callbacks()) enter_event(global_state.mods_at_last_key_or_button_event); + if (is_window_ready_for_callbacks()) enter_event(global_state.mods_at_last_key_or_button_event, cursor_moved); } else { debug_input("Mouse cursor left window: %llu\n", global_state.callback_os_window->id); if (is_window_ready_for_callbacks()) leave_event(global_state.mods_at_last_key_or_button_event); diff --git a/kitty/mouse.c b/kitty/mouse.c index 4e6682ecb..602eb9241 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -181,7 +181,7 @@ update_scrollbar_hover_state(Window *w, bool hovering) { } static void -set_currently_hovered_window(id_type window_id, int modifiers) { +set_currently_hovered_window(id_type window_id, int modifiers, bool focus_follows) { if (global_state.mouse_hover_in_window != window_id) { Window *left_window = window_for_id(global_state.mouse_hover_in_window); global_state.mouse_hover_in_window = window_id; @@ -196,7 +196,7 @@ set_currently_hovered_window(id_type window_id, int modifiers) { debug("Sent mouse leave event to window: %llu currently hovering: %llu\n", left_window->id, window_id); } } - if (window_id && OPT(focus_follows_mouse).on_cross && global_state.callback_os_window && global_state.callback_os_window->num_tabs) { + if (focus_follows && window_id && OPT(focus_follows_mouse).on_cross && global_state.callback_os_window && global_state.callback_os_window->num_tabs) { Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; for (unsigned i = 0; i < t->num_windows; i++) { if (t->windows[i].id == window_id) { @@ -934,7 +934,7 @@ currently_pressed_button(void) { HANDLER(handle_event) { modifiers &= ~GLFW_LOCK_MASK; set_mouse_cursor_for_screen(w->render_data.screen); - set_currently_hovered_window(w->id, modifiers); + set_currently_hovered_window(w->id, modifiers, true); if (button == -1) { button = currently_pressed_button(); handle_move_event(w, button, modifiers, window_idx); @@ -955,7 +955,7 @@ handle_window_title_bar_mouse(Window *w, int button, int modifiers, int action) static void handle_tab_bar_mouse(int button, int modifiers, int action) { - set_currently_hovered_window(0, modifiers); + set_currently_hovered_window(0, modifiers, false); OSWindow *w = global_state.callback_os_window; // dont report motion events, as they are expensive and useless if (w && (button > -1 || global_state.tab_being_dragged.id)) { @@ -1148,11 +1148,11 @@ update_mouse_pointer_shape(void) { void leave_event(int modifiers) { if (global_state.redirect_mouse_handling || global_state.active_drag_in_window || global_state.tracked_drag_in_window) return; - set_currently_hovered_window(0, modifiers); + set_currently_hovered_window(0, modifiers, false); } void -enter_event(int modifiers) { +enter_event(int modifiers, bool cursor_moved) { #ifdef __APPLE__ // On cocoa there is no way to configure the window manager to // focus windows on mouse enter, so we do it ourselves @@ -1170,7 +1170,7 @@ enter_event(int modifiers) { if (global_state.redirect_mouse_handling || global_state.active_drag_in_window || global_state.tracked_drag_in_window) return; MouseRegion r = mouse_region(false, false); Window *w = r.window; - set_currently_hovered_window(w ? w->id : 0, modifiers); + set_currently_hovered_window(w ? w->id : 0, modifiers, cursor_moved); if (!w || r.in_tab_bar || r.in_title_bar) return; if (handle_scrollbar_mouse(w, -1, MOVE, modifiers)) return; @@ -1359,7 +1359,7 @@ mouse_event(const int button, int modifiers, int action) { } MouseRegion r = mouse_region(true, true); w = r.window; window_idx = r.window_idx; - set_currently_hovered_window(w && !r.window_border && !r.in_title_bar ? w->id : 0, modifiers); + set_currently_hovered_window(w && !r.window_border && !r.in_title_bar ? w->id : 0, modifiers, true); if (r.in_tab_bar || global_state.tab_being_dragged.id) { mouse_cursor_shape = POINTER_POINTER;