Skip to content

fix(focus): restore focused window after workspace switch - #4

Merged
azytar merged 1 commit into
mainfrom
fix/workspace-switch-focus
Aug 16, 2026
Merged

fix(focus): restore focused window after workspace switch#4
azytar merged 1 commit into
mainfrom
fix/workspace-switch-focus

Conversation

@azytar

@azytar azytar commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Root cause: in the X11 backend, WindowManager::focus issued set_input_focus(w) and then called reconcile_focus() to verify the server accepted it, but only committed the logical focus (mon.focused / focus_stack) AFTER that reconcile. reconcile_focus compares the real X focus against mon.focused, so when the focus request's caller had not pre-written mon.focused (e.g. ViewWorkspace, which emits FocusWindow(best_focus(mi)) without updating mon.focused itself — unlike FocusDirection), mon.focused still named the window focused on the just-left workspace. reconcile_focus then saw logical != real and re-asserted input focus onto that previous, now hidden-but-viewable window, leaving real X focus parked on a hidden window while Maverick believed the visible one was focused. The only recovery was a later h/l press, which pre-sets mon.focused and so makes the reconcile a no-op.

Fix: commit mon.focused / focus_stack BEFORE reconcile_focus() in focus(), so the logical focus already matches the window we just focused and the reconcile is a no-op. This matches the ordering already used by the focus(None) branch and fixes every FocusWindow emitter uniformly without touching core state or X calls in core.

Verified: full cargo test -p maverick (343 tests, including the property/chaos invariant harness) passes; cargo check --workspace passes. The X-level reconciliation is exercised under Xephyr via the input-trace diagnostics. Added a core state/effect regression test (view_workspace_round_trip_keeps_focused_window) guarding the ViewWorkspace focus contract.

Root cause: in the X11 backend, WindowManager::focus issued
set_input_focus(w) and then called reconcile_focus() to verify the
server accepted it, but only committed the logical focus
(mon.focused / focus_stack) AFTER that reconcile. reconcile_focus
compares the real X focus against mon.focused, so when the focus
request's caller had not pre-written mon.focused (e.g. ViewWorkspace,
which emits FocusWindow(best_focus(mi)) without updating mon.focused
itself — unlike FocusDirection), mon.focused still named the window
focused on the just-left workspace. reconcile_focus then saw
logical != real and re-asserted input focus onto that previous, now
hidden-but-viewable window, leaving real X focus parked on a hidden
window while Maverick believed the visible one was focused. The only
recovery was a later h/l press, which pre-sets mon.focused and so makes
the reconcile a no-op.

Fix: commit mon.focused / focus_stack BEFORE reconcile_focus() in
focus(), so the logical focus already matches the window we just focused
and the reconcile is a no-op. This matches the ordering already used by
the focus(None) branch and fixes every FocusWindow emitter uniformly
without touching core state or X calls in core.

Verified: full cargo test -p maverick (343 tests, including the
property/chaos invariant harness) passes; cargo check --workspace
passes. The X-level reconciliation is exercised under Xephyr via the
input-trace diagnostics. Added a core state/effect regression test
(view_workspace_round_trip_keeps_focused_window) guarding the
ViewWorkspace focus contract.
@azytar
azytar merged commit 5bcb1f1 into main Aug 16, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant