@@ -970,27 +970,39 @@ impl WmState { |
| 970 | 970 | return Some(id); |
| 971 | 971 | } |
| 972 | 972 | |
| 973 | | - for ws in self.workspaces.iter() { |
| 974 | | - for &wid in ws.focus_history.iter().rev() { |
| 975 | | - if self |
| 976 | | - .registry |
| 977 | | - .get(wid) |
| 978 | | - .is_some_and(|window| window.app_pid == pid) |
| 979 | | - && self.is_external_focus_candidate(wid) |
| 980 | | - { |
| 981 | | - return Some(wid); |
| 982 | | - } |
| 973 | + // Prefer windows on currently visible workspaces over hidden ones, and |
| 974 | + // the focused monitor's workspace over other visible ones. Without this |
| 975 | + // bias the resolver iterates by workspace index, so workspace 1 (index |
| 976 | + // 0) acts as a magnet for any cross-workspace external focus event and |
| 977 | + // can yank the WM back across workspaces it just left. |
| 978 | + let active_ws = self |
| 979 | + .monitors |
| 980 | + .get(self.focused_monitor) |
| 981 | + .map(|m| m.active_workspace); |
| 982 | + |
| 983 | + if let Some(ws_idx) = active_ws |
| 984 | + && let Some(wid) = self.find_pid_target_in_workspace(ws_idx, pid) |
| 985 | + { |
| 986 | + return Some(wid); |
| 987 | + } |
| 988 | + |
| 989 | + for (ws_idx, _) in self.workspaces.iter().enumerate() { |
| 990 | + if Some(ws_idx) == active_ws { |
| 991 | + continue; |
| 992 | + } |
| 993 | + if self.monitor_showing_workspace(ws_idx).is_none() { |
| 994 | + continue; |
| 995 | + } |
| 996 | + if let Some(wid) = self.find_pid_target_in_workspace(ws_idx, pid) { |
| 997 | + return Some(wid); |
| 983 | 998 | } |
| 984 | 999 | } |
| 985 | 1000 | |
| 986 | | - for ws in self.workspaces.iter() { |
| 987 | | - if let Some(wid) = ws.focused |
| 988 | | - && self |
| 989 | | - .registry |
| 990 | | - .get(wid) |
| 991 | | - .is_some_and(|window| window.app_pid == pid) |
| 992 | | - && self.is_external_focus_candidate(wid) |
| 993 | | - { |
| 1001 | + for (ws_idx, _) in self.workspaces.iter().enumerate() { |
| 1002 | + if self.monitor_showing_workspace(ws_idx).is_some() { |
| 1003 | + continue; |
| 1004 | + } |
| 1005 | + if let Some(wid) = self.find_pid_target_in_workspace(ws_idx, pid) { |
| 994 | 1006 | return Some(wid); |
| 995 | 1007 | } |
| 996 | 1008 | } |
@@ -1002,6 +1014,21 @@ impl WmState { |
| 1002 | 1014 | .find(|id| self.is_external_focus_candidate(*id)) |
| 1003 | 1015 | } |
| 1004 | 1016 | |
| 1017 | + fn find_pid_target_in_workspace(&self, ws_idx: usize, pid: i32) -> Option<WindowId> { |
| 1018 | + let ws = self.workspaces.get(ws_idx); |
| 1019 | + for &wid in ws.focus_history.iter().rev() { |
| 1020 | + if self |
| 1021 | + .registry |
| 1022 | + .get(wid) |
| 1023 | + .is_some_and(|window| window.app_pid == pid) |
| 1024 | + && self.is_external_focus_candidate(wid) |
| 1025 | + { |
| 1026 | + return Some(wid); |
| 1027 | + } |
| 1028 | + } |
| 1029 | + None |
| 1030 | + } |
| 1031 | + |
| 1005 | 1032 | fn adopt_external_focus(&mut self, id: WindowId) { |
| 1006 | 1033 | let Some(ws_idx) = self.workspaces.find_window(id) else { |
| 1007 | 1034 | return; |
@@ -3695,6 +3722,52 @@ mod tests { |
| 3695 | 3722 | assert!(state.ffm_cooldown_until.is_none()); |
| 3696 | 3723 | } |
| 3697 | 3724 | |
| 3725 | + #[test] |
| 3726 | + fn external_app_focus_prefers_visible_workspace_over_workspace_one() { |
| 3727 | + // Regression: a stale focus_history entry for `pid` on workspace 1 |
| 3728 | + // (index 0) used to win over a candidate on the currently visible |
| 3729 | + // workspace, yanking the WM back to ws1 in a feedback loop with the |
| 3730 | + // 50ms frontmost-app poll. |
| 3731 | + let mut state = WmState::new(); |
| 3732 | + state.monitors = vec![Monitor { |
| 3733 | + id: 42, |
| 3734 | + frame: Rect::new(0.0, 0.0, 1920.0, 1080.0), |
| 3735 | + usable_frame: Rect::new(0.0, 33.0, 1920.0, 1047.0), |
| 3736 | + is_primary: true, |
| 3737 | + active_workspace: 0, |
| 3738 | + }]; |
| 3739 | + let screen = state.monitors[0].usable_frame; |
| 3740 | + |
| 3741 | + state.registry.add(tracked_window(70, 13, "WezTerm")); |
| 3742 | + state.registry.add(tracked_window(71, 13, "WezTerm")); |
| 3743 | + state |
| 3744 | + .workspaces |
| 3745 | + .get_or_create_target(&WorkspaceTarget::Numbered(2)); |
| 3746 | + |
| 3747 | + // Stale history on ws1 for pid 13. |
| 3748 | + { |
| 3749 | + let ws = state.workspaces.get_mut(0); |
| 3750 | + ws.tree.insert_with_rect(70, None, screen); |
| 3751 | + ws.record_focus(70); |
| 3752 | + } |
| 3753 | + // Currently visible ws2 also has a window for pid 13. |
| 3754 | + { |
| 3755 | + let ws = state.workspaces.get_mut(1); |
| 3756 | + ws.tree.insert_with_rect(71, None, screen); |
| 3757 | + ws.record_focus(71); |
| 3758 | + } |
| 3759 | + |
| 3760 | + // Make ws2 the visible workspace. |
| 3761 | + state.monitors[0].active_workspace = 1; |
| 3762 | + state.sync_workspace_visibility(); |
| 3763 | + |
| 3764 | + state.adopt_external_app_focus(13, None); |
| 3765 | + |
| 3766 | + // Resolver must prefer the ws2 window over the stale ws1 entry. |
| 3767 | + assert_eq!(state.monitors[0].active_workspace, 1); |
| 3768 | + assert_eq!(state.active_workspace().focused, Some(71)); |
| 3769 | + } |
| 3770 | + |
| 3698 | 3771 | #[test] |
| 3699 | 3772 | fn external_app_focus_dismisses_overlay_before_switching() { |
| 3700 | 3773 | let mut state = WmState::new(); |