pinnacle icon indicating copy to clipboard operation
pinnacle copied to clipboard

Pointer and keyboard focus are not the same

Open anriha opened this issue 3 months ago • 6 comments

I've been working on implementing a proper sloppy focus policy in my config and have run into a limitation with the current focus model. I wanted to open this issue to discuss a potential architectural improvement.

The Problem

The current example for sloppy focus works for basic cases, but fails in a common scenario:

  1. Keyboard focus is on Window A.
  2. The user switches keyboard focus to Window B without moving the pointer (via key bindings/layout change).
  3. The user then moves the pointer over Window A.

In this case, focus remains incorrectly on Window B. The root cause is that the PointerEnter signal only fires when the pointer focus changes (i.e., when crossing a surface boundary), but it doesn't take the compositor's actual keyboard focus into account.

Attempted Solution

My first thought was to patch the signal logic to be aware of keyboard focus. I've implemented that here: https://github.com/pinnacle-comp/pinnacle/commit/a09c56bdbd55b2a20e11fe4b1f1737347a2edf64

While this commit does make sloppy focus work as expected, it feels like the wrong approach. It changes the fundamental meaning of the PointerEnter signal, which could have unintended side effects for other use cases or for users who prefer click-to-focus.

Proposed Architectural Solution

Instead of patching the signals, I believe a better, more robust solution would be to unify the pointer and keyboard focus models at the compositor's core.

This would make sloppy focus a built-in, reliable behavior, and the PointerEnter/Leave signals could return to their simple, pure meaning of boundary crossings.

I'm happy to work on a PR to implement this unified model if you agree that this is a sensible direction for the project. Looking forward to your thoughts.

anriha avatar Sep 21 '25 19:09 anriha

The current example for sloppy focus works for basic cases, but fails in a common scenario:

  1. Keyboard focus is on Window A.
  2. The user switches keyboard focus to Window B without moving the pointer (via key bindings/layout change).
  3. The user then moves the pointer over Window A.

In this case, focus remains incorrectly on Window B. The root cause is that the PointerEnter signal only fires when the pointer focus changes (i.e., when crossing a surface boundary), but it doesn't take the compositor's actual keyboard focus into account.

I'm not sure I understand what's the problem with the behavior you're describing.

In my opinion, needing a click or at the very least leaving & re-entering the surface is better if the focus was overridden through bindings. If I override the focus using keybindings, I don't want it to randomly change because I bumped the mouse/touched a laptop touchpad.

Ph4ntomas avatar Sep 21 '25 22:09 Ph4ntomas

That is a very fair point. Supporting both focus models is definitely the ideal outcome.

My experience with sloppy focus on other compositors has been that the focus always follows the pointer, but I can see the value in both behaviors. The main issue is that the current signaling model doesn't seem to provide a way to implement this "truer" sloppy focus, even for users who opt into it. The PointerEnter signal is insufficient because of the desynchronization between pointer and keyboard focus.

Something like WindowSignal::PointerMotion could work, as it would give the config the necessary information to enforce the focus change. However, I don't like this solution as that would be very "chatty" signal.

Perhaps there is a more targeted solution. What if we introduced a new, more specific signal? Something like:

WindowSignal::PointerFocusChanged Which would take into account both keyboard and pointer focus. That way, users could use this signal if they want the sloppy focus I described. Or they could use the current way.

anriha avatar Sep 21 '25 23:09 anriha

My experience with sloppy focus on other compositors has been that the focus always follows the pointer.

I'm used to awesomewm, and it has the same behavior for sloppy focus. Do the other compositor you mention supports setting focus via keybind ?

WindowSignal::PointerFocusChanged Which would take into account both keyboard and pointer focus. That way, users could use this signal if they want the sloppy focus I described. Or they could use the current way.

Yeah, this might be the best solution imo, although I'm not sure what you mean by 'both keyboard and pointer focus'. IMO this event should be raised once if the mouse move while over an unfocused window (that would then be reset when the focused window change, so the config doesn't get spammed needlessy)

Ph4ntomas avatar Sep 22 '25 00:09 Ph4ntomas

I've used sloppy focus in other compositors (qtile, river, dwl, river) and all of those do work as I suggest (as far as I can remember it has been while with some of them :) ) and they do all support setting focus via keybindings. For example, my personal workflow is a great example of why both are useful: when I'm coding, I use keybindings exclusively to manage focus. But when I do need the mouse, I find it much more intuitive for the focus to follow the pointer without needing an extra click.

Yeah, this might be the best solution imo, although I'm not sure what you mean by 'both keyboard and pointer focus'. IMO this event should be raised once if the mouse move while over an unfocused window (that would then be reset when the focused window change, so the config doesn't get spammed needlessy)

The current issue is that Pinnacle currently has two completely separate focus states: one for the keyboard and one for the pointer. To implement the fire-once signal, these two states would need to be connected to some extent, so the pointer logic can be aware of the keyboard focus.

My original idea was to unify them completely, but you raised valid points about preserving the click-to-focus model. The alternative is to track the keyboard focus state within the pointer handler to generate the new event, but perhaps there's a cleaner way to bridge them that I'm not seeing.

anriha avatar Sep 22 '25 00:09 anriha

I'd rather not build in specific sloppy focus models because a) it doesn't allow finer control, and b) there would be multiple ways to have sloppy focus and that's less than ideal.

I'd also rather not plumb in the logic for a specific signal that's just for this "sloppier" focus and have to keep track of the pointer moving after a keyboard focus change.

There's this awesome issue that's pretty much the same, and I agree with the solutions presented there—mainly to use a pointer motion signal. However you're right that it would be a very spammy signal which is why it hasn't been added yet. I think to use a motion signal for this use case we'd need to:

  1. Add a window pointer motion signal with some form of throttle, maybe only reporting motion every frame or even less frequently than that
    • This alone is probably sufficient to enable sloppier focus.
  2. For performance, arm a oneshot signal connection whenever the keyboard focus changes that runs once, then immediately disconnects.
    • We could probably arm it in WindowSignal::Focused. That way we're only running the signal callback once after the focus changes.
    • I will note that the current process for arming a signal that disconnects itself after getting called is a little unwieldy, could probably add some helper functions for that

Ottatop avatar Sep 23 '25 21:09 Ottatop

I think that makes sense. Using pointer movement would be the cleanest solution. Even if throttled to the refresh rate, that should still have basically zero impact. The signal could send the window below the pointer, or it might be better to add api for the pointer itself. Because I could see other uses for the functionality get window below the pointer.

anriha avatar Sep 23 '25 22:09 anriha