From 1fb94d32f66dec3324f99224225fc1133a703448 Mon Sep 17 00:00:00 2001 From: chris-010 <10660568+chris-010@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:35:19 +0200 Subject: [PATCH] fix: reconcile focus-follows-cursor after workspace switch With "focus follows cursor" enabled, switching workspaces left keyboard focus on the newly-active workspace's previously-active window instead of the window under the pointer. Neither staying still nor moving the pointer within that window corrected it -- only a click did. Focus-follows-cursor is implemented solely as an edge-triggered pointer motion handler that compares the element under the old vs new pointer position. A workspace switch moves focus without any pointer motion, so the handler never runs, and the switch actions only call Shell::activate() and never consult focus_follows_cursor. Add State::reconcile_focus_to_pointer(), invoked from the four workspace-switch actions (Workspace, LastWorkspace, NextWorkspace, PreviousWorkspace) on success. When focus-follows-cursor is enabled and the pointer is ungrabbed, it focuses the window under the pointer on the destination workspace, and leaves focus untouched when the pointer is over empty space. It queries the destination workspace's layout directly via Workspace::toplevel_element_under rather than State::element_under: the switch animation (WorkspaceDelta::Shortcut) is still in progress when the action handler runs, and element_under honours the animation offset, so it would resolve the pointer to the outgoing workspace's window. Focusing that off-workspace window is then reverted by Common::refresh_focus to the new workspace's focus-stack top, defeating the reconcile. active_space() is already the destination workspace (the active index is updated synchronously), so its toplevel_element_under ignores the animation and yields the window actually under the pointer; set_focus() then makes it the workspace's focus-stack top, which refresh_focus subsequently keeps. MoveToWorkspace/SendToWorkspace are deliberately left untouched: the moved window should follow and stay focused. Fixes: pop-os/cosmic-epoch#3650 This change was drafted with AI assistance (Claude Code); I have reviewed it, understand it in full, and can respond to review. Signed-off-by: chris-010 <10660568+chris-010@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 --- src/input/actions.rs | 15 +++++++++++-- src/input/mod.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/input/actions.rs b/src/input/actions.rs index 3d017697d..b1e015aee 100644 --- a/src/input/actions.rs +++ b/src/input/actions.rs @@ -183,24 +183,31 @@ impl State { 0 => 9, x => x - 1, }; - let _ = self.common.shell.write().activate( + let res = self.common.shell.write().activate( ¤t_output, workspace as usize, WorkspaceDelta::new_shortcut(), &mut self.common.workspace_state.update(), ); + if res.is_ok() { + self.reconcile_focus_to_pointer(seat); + } } Action::LastWorkspace => { let current_output = seat.active_output(); let mut shell = self.common.shell.write(); let workspace = shell.workspaces.len(¤t_output).saturating_sub(1); - let _ = shell.activate( + let res = shell.activate( ¤t_output, workspace, WorkspaceDelta::new_shortcut(), &mut self.common.workspace_state.update(), ); + drop(shell); + if res.is_ok() { + self.reconcile_focus_to_pointer(seat); + } } Action::NextWorkspace => { @@ -239,6 +246,8 @@ impl State { direction, true, ) + } else if next.is_ok() { + self.reconcile_focus_to_pointer(seat); } } @@ -278,6 +287,8 @@ impl State { direction, true, ) + } else if previous.is_ok() { + self.reconcile_focus_to_pointer(seat); }; } diff --git a/src/input/mod.rs b/src/input/mod.rs index 39bff9dc0..b4b0f4260 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -2073,6 +2073,56 @@ impl State { } } + /// Reconcile keyboard focus to the window under the pointer on the active workspace. + /// + /// Does nothing unless focus-follows-cursor is enabled and the pointer is + /// ungrabbed, and never clears focus when the pointer is over empty space. + pub fn reconcile_focus_to_pointer(&mut self, seat: &Seat) { + if !self.common.config.cosmic_conf.focus_follows_cursor { + return; + } + + let Some(ptr) = seat.get_pointer() else { + return; + }; + if ptr.is_grabbed() { + return; + } + + let position = ptr.current_location().as_global(); + let target = { + let shell = self.common.shell.read(); + let Some(output) = shell + .outputs() + .find(|output| output.geometry().to_f64().contains(position)) + .cloned() + else { + return; + }; + shell + .active_space(&output) + .and_then(|workspace| workspace.toplevel_element_under(position, seat)) + }; + + let Some(target) = target else { + return; + }; + + if let Some(pointer_focus_state) = self.common.pointer_focus_state.take() { + self.common + .event_loop_handle + .remove(pointer_focus_state.token); + } + + Shell::set_focus( + self, + Some(&target), + seat, + Some(SERIAL_COUNTER.next_serial()), + false, + ); + } + #[profiling::function] pub fn element_under( global_pos: Point,