Add option to disable auto-raise for focus-follows-cursor - #2612
Open
perpetualbits wants to merge 4 commits into
Open
Add option to disable auto-raise for focus-follows-cursor#2612perpetualbits wants to merge 4 commits into
perpetualbits wants to merge 4 commits into
Conversation
This was referenced Jul 21, 2026
Assisted-by: Claude Code (Anthropic) Signed-off-by: Roland Nagtegaal <perpetualbits@gmail.com>
Assisted-by: Claude Code (Anthropic) Signed-off-by: Roland Nagtegaal <perpetualbits@gmail.com>
Add a Raise intent enum threaded through Shell::set_focus and a single persisted no-raise window mark on Shell. update_active() skips raising the marked window, so focus-follows-cursor can move focus without lifting the window (sloppy focus) when focus_follows_cursor_raise is false. Explicit focus (click, keyboard, activation) passes Raise::Yes and still raises. Assisted-by: Claude Code (Anthropic) Signed-off-by: Roland Nagtegaal <perpetualbits@gmail.com>
Fixes a bug in the focus_follows_cursor_raise=false ("sloppy focus")
path: moving the pointer off a hovered floating window onto the empty
desktop raised that window to the front, even though raising was
disabled.
Root cause
----------
When the pointer settles, focus-follows-cursor schedules a focus change
for whatever element_under() returns at the cursor. Over the "empty"
desktop that is not None: it returns a non-window KeyboardFocusTarget
(a layer surface / desktop target). Because the target is Some, the
focus-delay timer fires and calls set_focus(target, Raise::No).
The no_raise_window mark is what tells update_active() to skip raising
the hovered window. Its previous logic set the mark only for Element
targets and fell through to `_ => None` for everything else, so focusing
the non-window desktop target *cleared* the mark. The hovered window was
still the focus-stack top, so the now-unsuppressed update_active() raised
it. This is why the raise happened exactly focus_follows_cursor_delay ms
after the pointer reached the desktop.
Fix
---
Passive focus (Raise::No) onto a non-window target must not clear the
suppression. Keep the existing mark in the `_` arm; it is still cleared
by any explicit focus (Raise::Yes: click, keyboard, activation).
Focusing another window (Element) still re-points the mark as before, so
window-to-window sloppy focus is unchanged.
How this was found and verified
-------------------------------
Reproduced on a real KMS session (focus-follows-cursor only fires under
the KMS backend). Temporary trace instrumentation was added at the four
decision points - motion boundary, FFC timer firing, set_focus mark
update, and update_active raise - and before/after logs were captured.
Before the fix, crossing onto the desktop logged
`set_focus ... mark_some=false` immediately followed by
`update_active RAISE (floating)`; after the fix it logs `mark_some=true`
and no raise, and the window visibly stays put. Window-to-window focus
continued to show `mark_some=true` with no spurious raise, confirming no
regression to the case that already worked.
Assisted-by: Claude Code (Anthropic)
Signed-off-by: Roland Nagtegaal <perpetualbits@gmail.com>
perpetualbits
force-pushed
the
focus-follows-cursor-no-raise-v2
branch
from
August 11, 2026 14:09
9509d3e to
447477c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #863. Companion settings PR: pop-os/cosmic-settings#2066.
This supersedes #2535: same feature, but with a bug (found and fixed during
review — see "Bug found and fixed" below) and a clearer write-up. #2535 will be
closed in favour of this one.
AI assistance disclosure
This change was developed with AI assistance (Claude Code); every commit carries
an
Assisted-by:trailer. Per your policy I'd rather state this openly than hideit. The design and the fix are ones I understand in full and can defend in review
(the intent-enum, the no-raise mark, and the root cause of the bug are all
explained below and in the commit messages), and everything was validated by me
on real hardware. Happy to walk through any part or change anything on request.
Summary
Adds a compositor config option,
focus_follows_cursor_raise(defaulttrue,preserving current behaviour), that decouples raising from focusing when focus
follows the cursor. With
focus_follows_cursoron andfocus_follows_cursor_raiseoff, moving the pointer over a floating window gives it keyboard focus without
raising it to the front ("sloppy focus"). Explicit focus — a click, a keyboard
focus action, or an application activating itself — still raises the window.
Motivation
Today focus and raising are coupled: any focus change runs
update_active(),which raises the focused floating window. With focus-follows-cursor that means a
window jumps to the front merely because the pointer crossed it. This option lets
the two behaviours be chosen independently — the long-standing "focus follows
mouse, no auto-raise" model many users prefer.
Implementation
focus_follows_cursor_raiseincosmic-comp-config(defaulttrue),with live reload wired into the existing config watcher.
Raise { Yes, No }intent enum threaded throughShell::set_focus. Anamed enum is used rather than a second
boolnext to the existingupdate_cursorargument, so call sites stay self-documenting and the compilerenforces completeness — a missed call site cannot build. Every explicit-focus
call site passes
Raise::Yes; only the focus-follows-cursor timer passes theconfigured choice.
no_raise_windowmark onShell. This is needed because focus isreconciled periodically:
update_active()is re-run and would re-raise thefocused floating window on the next pass, so suppressing the raise once is not
enough. The hovered window is recorded and
update_active()skips raising it,under the same write lock that runs
update_active()so the two never disagree.Bug found and fixed (during review of #2535)
The first version of this change had a bug: with raise disabled, moving the
pointer off a hovered floating window onto the empty desktop still raised that
window.
Root cause: over the "empty" desktop,
element_under()does not returnNone—it returns a non-window focus target (a layer surface / desktop target). The
focus-follows-cursor timer therefore fired and called
set_focus(target, Raise::No). The mark-setting logic only set the no-raise mark forElementtargets and fell through to
_ => Nonefor everything else, so focusing thenon-window desktop target cleared the mark. The hovered window was still the
focus-stack top, so the now-unsuppressed
update_active()raised it — exactlyfocus_follows_cursor_delayms after reaching the desktop.Fix (final commit): passive focus (
Raise::No) onto a non-window target keeps theexisting mark instead of clearing it; only explicit focus (
Raise::Yes) clearsit. This was reproduced and verified on real hardware with temporary trace
instrumentation (before: mark cleared → raise; after: mark preserved → no raise;
window-to-window focus unaffected).
Testing
cargo buildandcargo fmt --checkare clean.focus_follows_cursor_raise = true) is covered by a unit test incosmic-comp-config.focuses it without raising it; clicking still raises it; moving to the empty
desktop no longer raises it (the fixed bug); toggling the option at runtime
switches behaviour immediately (live reload).
Note: focus-follows-cursor only triggers under the KMS backend, since the
follow-cursor logic lives in the relative-motion (
PointerMotion) handler; thenested
winit/x11backends deliver absolute motion, so this must be exercisedon a real session rather than nested.
Note on scope
The no-raise mark is a single value on
Shellrather than per-seat. In the commonsingle-seat case this is exact. In a multi-seat setup, one seat's explicit focus
clears another seat's hover mark, so the other seat's hovered window can be
re-raised on the next reconciliation. This keeps the change small; happy to make
it per-seat if preferred.