fix(wm): refocus selection on destroy when foreground goes null

Reactive events pass trigger_focus=false so komorebi follows the OS
foreground and reconciles via the FocusChange handler once Windows
promotes a successor window. When the destroyed window was the
foreground and Windows promotes nothing, the foreground becomes null,
no FocusChange arrives, and komorebi's selection never regains OS focus,
leaving every border unfocused until the user refocuses by hand.

Add a narrow fallback in the Destroy/Unmanage handler: when the OS
foreground is null after removal, focus the current selection. Any
non-null foreground is left to FocusChange, so the OS is not fought and
the should_skip_focus_change desync cannot occur.
This commit is contained in:
Matt Kotsenas
2026-07-22 14:14:56 -07:00
committed by Jeezy
parent fd3d52c477
commit 8a6868f2d2
+37 -1
View File
@@ -45,6 +45,16 @@ fn should_skip_focus_change(foreground_hwnd: Option<isize>, window_hwnd: isize)
matches!(foreground_hwnd, Some(hwnd) if hwnd != window_hwnd)
}
/// After a managed window is destroyed, Windows may promote no successor, leaving
/// the foreground null with no FocusChange to reconcile from. The selection then
/// never regains focus: every border renders unfocused and keystrokes fall through
/// to nothing until the user refocuses by hand. Refocus only in that case, otherwise
/// foreground successor is left to FocusChange so we don't fight the OS or desync
/// focus.
fn should_refocus_after_destroy(foreground_hwnd: Option<isize>) -> bool {
matches!(foreground_hwnd, None | Some(0))
}
#[cfg(test)]
mod focus_change_tests {
use super::should_skip_focus_change;
@@ -65,6 +75,26 @@ mod focus_change_tests {
}
}
#[cfg(test)]
mod destroy_focus_tests {
use super::should_refocus_after_destroy;
#[test]
fn refocuses_when_foreground_is_null() {
assert!(should_refocus_after_destroy(Some(0)));
}
#[test]
fn refocuses_when_foreground_is_unavailable() {
assert!(should_refocus_after_destroy(None));
}
#[test]
fn defers_when_a_successor_took_the_foreground() {
assert!(!should_refocus_after_destroy(Some(12345)));
}
}
#[tracing::instrument]
pub fn listen_for_events(wm: Arc<Mutex<WindowManager>>) {
let receiver = wm.lock().incoming_events.clone();
@@ -282,7 +312,13 @@ impl WindowManager {
WindowManagerEvent::Destroy(_, window) | WindowManagerEvent::Unmanage(window) => {
if self.focused_workspace()?.contains_window(window.hwnd) {
self.focused_workspace_mut()?.remove_window(window.hwnd)?;
self.update_focused_workspace(false, false)?;
// With refocus true, update_focused_workspace focuses the surviving
// selection (maximized/monocle/tiling); with refocus false it only
// re-tiles.
let refocus =
should_refocus_after_destroy(WindowsApi::foreground_window().ok());
self.update_focused_workspace(refocus, refocus)?;
let mut already_moved_window_handles = self.already_moved_window_handles.lock();