Skip to content

Add option to disable auto-raise for focus-follows-cursor - #2612

Open
perpetualbits wants to merge 4 commits into
pop-os:masterfrom
perpetualbits:focus-follows-cursor-no-raise-v2
Open

Add option to disable auto-raise for focus-follows-cursor#2612
perpetualbits wants to merge 4 commits into
pop-os:masterfrom
perpetualbits:focus-follows-cursor-no-raise-v2

Conversation

@perpetualbits

Copy link
Copy Markdown
  • I have disclosed use of any AI generated code in my commit messages.
  • I understand these changes in full and will be able to respond to review comments.
  • My change is accurately described in the commit message.
  • My contribution is tested and working as described.
  • I have read the Developer Certificate of Origin and certify my contribution under its conditions.

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 hide
it. 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 (default true,
preserving current behaviour), that decouples raising from focusing when focus
follows the cursor. With focus_follows_cursor on and focus_follows_cursor_raise
off, 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

  • New field focus_follows_cursor_raise in cosmic-comp-config (default true),
    with live reload wired into the existing config watcher.
  • A small Raise { Yes, No } intent enum threaded through Shell::set_focus. A
    named enum is used rather than a second bool next to the existing
    update_cursor argument, so call sites stay self-documenting and the compiler
    enforces completeness — a missed call site cannot build. Every explicit-focus
    call site passes Raise::Yes; only the focus-follows-cursor timer passes the
    configured choice.
  • A persisted no_raise_window mark on Shell. This is needed because focus is
    reconciled periodically: update_active() is re-run and would re-raise the
    focused 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 return None
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 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 — exactly
focus_follows_cursor_delay ms after reaching the desktop.

Fix (final commit): passive focus (Raise::No) onto a non-window target keeps the
existing mark instead of clearing it; only explicit focus (Raise::Yes) clears
it. 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 build and cargo fmt --check are clean.
  • The default (focus_follows_cursor_raise = true) is covered by a unit test in
    cosmic-comp-config.
  • Validated on a real KMS session. Hovering a partly-covered floating window
    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; the
nested winit/x11 backends deliver absolute motion, so this must be exercised
on a real session rather than nested.

Note on scope

The no-raise mark is a single value on Shell rather than per-seat. In the common
single-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.

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
perpetualbits force-pushed the focus-follows-cursor-no-raise-v2 branch from 9509d3e to 447477c Compare August 11, 2026 14:09
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