Skip to content

Commit

Permalink
Add implicit pointer grabs in Wayland. (#4540)
Browse files Browse the repository at this point in the history
* Add implicit pointer grabs in Wayland.

A Wayland client expects to receive pointer motion events from the time
a button is pressed until it is released, even if the pointer leaves the
client's surface while the button is held.  This is crucial for
scrollbar functionality: a scrollbar should continue to drag even if the
pointer leaves the window.  This does not currently work in qtile.  For
instance, in Chrome, if you drag the scrollbar, move the mouse off the
Chrome window, release the button, then move the pointer back onto the
Chrome window, Chrome will resume scrolling without any button press.

In X11, this functionality is implemented on the client side using
XGrabPointer or XIGrabDevice.  Wayland provides no such mechanism: this
behavior must be implemented in the compositor.  This does not seem to
be well documented.  However, if you look in gdk_wayland_device_grab in
the gtk source, you'll see that grabbing the pointer in GDK is basically
a no-op: it does some internal accounting and changes the cursor, then
simply assumes the pointer is already grabbed.

Implicit grabs are implemented in Sway (see
sway/sway/input/seatop_{default,down}.c), although that's not what
they're called.  In short, pressing a button on a client surface
switches the cursor handling code into "down" mode, which short-cuts
most of the WM pointer processing logic and sends events directly to the
client, until the button is released.  (This is part of a more general
mechanism in Sway: pointer events can be processed in several modes,
e.g. for moving floating windows.)

This commit implements basic implicit grabs in qtile Wayland, following
what Sway does (functionally, not structurally).  Pressing a button
creates an ImplicitGrab object in the Core; releasing the button
destroys it.  While the ImplicitGrab object is alive, all pointer
events (button, {absolute_,}motion, axis) are sent directly to wlroots,
bypassing qtile's event processing.  This seems to produce the desired
UI behavior.

Implicit grabs should also be implemented for touch and tablet tool
events, if and when those are added to qtile.

* Fix implicit pointer grab in Wayland.

Move grab initation until *after* giving qtile a chance to process the
button event.

* Tweaked implicit pointer grabs in Wayland.

---------

Co-authored-by: Joe Rabinoff <rabinoff@post.harvard.edu>
  • Loading branch information
QBobWatson and Joe Rabinoff committed Oct 29, 2023
1 parent 01ebe18 commit 9864ed9
Showing 1 changed file with 81 additions and 6 deletions.
87 changes: 81 additions & 6 deletions libqtile/backend/wayland/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,36 @@
from libqtile.core.manager import Qtile


class ImplicitGrab(wlrq.HasListeners):
"""Keep track of an implicit pointer grab.
A Wayland client expects to receive pointer events from the moment a
pointer button is pressed on its surface until the moment the button is
released. The Wayland protocol leaves this behavior to the compositor.
"""

def __init__(
self,
core: Core,
surface: Surface,
start_x: int,
start_y: int,
start_sx: int,
start_sy: int,
) -> None:
self.core = core
self.surface = surface
self.start_dx = start_sx - start_x
self.start_dy = start_sy - start_y
self.add_listener(surface.destroy_event, self._on_destroy)

def finalize(self) -> None:
self.finalize_listeners()

def _on_destroy(self, _listener: Listener, _data: Any) -> None:
self.core._release_implicit_grab()


class Core(base.Core, wlrq.HasListeners):
supports_restarting: bool = False

Expand Down Expand Up @@ -198,6 +228,8 @@ def __init__(self) -> None:
self.cursor = Cursor(self.output_layout)
self.cursor_manager = XCursorManager(24)
self._gestures = PointerGesturesV1(self.display)
self._pressed_button_count = 0
self._implicit_grab: ImplicitGrab | None = None
self.add_listener(self.seat.request_set_cursor_event, self._on_request_cursor)
self.add_listener(self.cursor.axis_event, self._on_cursor_axis)
self.add_listener(self.cursor.frame_event, self._on_cursor_frame)
Expand Down Expand Up @@ -549,10 +581,23 @@ def _on_new_xdg_surface(self, _listener: Listener, xdg_surface: XdgSurface) -> N

logger.warning("xdg_shell surface had no role set. Ignoring.")

def _release_implicit_grab(self, time: int) -> None:
if self._implicit_grab is not None:
logger.debug("Releasing implicit grab.")
self._implicit_grab.finalize()
self._implicit_grab = None
# Pretend the cursor just appeared where it is.
self._process_cursor_motion(time, self.cursor.x, self.cursor.y)

def _create_implicit_grab(self, time: int, surface: Surface, sx: float, sy: float) -> None:
self._release_implicit_grab(time)
logger.debug("Creating implicit grab.")
self._implicit_grab = ImplicitGrab(self, surface, self.cursor.x, self.cursor.y, sx, sy)

def _on_cursor_axis(self, _listener: Listener, event: pointer.PointerAxisEvent) -> None:
handled = False

if event.delta != 0 and not self.exclusive_client:
if event.delta != 0 and not self.exclusive_client and not self._implicit_grab:
# If we have a client who exclusively gets input, button bindings are disallowed.
if event.orientation == pointer.AxisOrientation.VERTICAL:
button = 5 if 0 < event.delta else 4
Expand All @@ -575,9 +620,21 @@ def _on_cursor_frame(self, _listener: Listener, _data: Any) -> None:
def _on_cursor_button(self, _listener: Listener, event: pointer.PointerButtonEvent) -> None:
assert self.qtile is not None
self.idle.notify_activity(self.seat)
found = None
pressed = event.button_state == input_device.ButtonState.PRESSED
if pressed:
self._focus_by_click()
self._pressed_button_count += 1
if self._implicit_grab is None:
found = self._focus_by_click()
else:
if self._pressed_button_count > 0: # sanity check
self._pressed_button_count -= 1

if self._implicit_grab is not None:
self.seat.pointer_notify_button(event.time_msec, event.button, event.button_state)
if self._pressed_button_count == 0:
self._release_implicit_grab(event.time_msec)
return

handled = False

Expand All @@ -587,8 +644,17 @@ def _on_cursor_button(self, _listener: Listener, event: pointer.PointerButtonEve
handled = self._process_cursor_button(button, pressed)

if not handled:
if self._pressed_button_count == 1 and found:
win, surface, sx, sy = found
if surface:
self._create_implicit_grab(event.time_msec, surface, sx, sy)
self.seat.pointer_notify_button(event.time_msec, event.button, event.button_state)

def _implicit_grab_motion(self, time: int) -> None:
sx = self.cursor.x + self._implicit_grab.start_dx
sy = self.cursor.y + self._implicit_grab.start_dy
self.seat.pointer_notify_motion(time, sx, sy)

def _on_cursor_motion(self, _listener: Listener, event: pointer.PointerMotionEvent) -> None:
assert self.qtile is not None
self.idle.notify_activity(self.seat)
Expand All @@ -614,7 +680,11 @@ def _on_cursor_motion(self, _listener: Listener, event: pointer.PointerMotionEve
return

self.cursor.move(dx, dy)
self._process_cursor_motion(event.time_msec, self.cursor.x, self.cursor.y)

if self._implicit_grab is None:
self._process_cursor_motion(event.time_msec, self.cursor.x, self.cursor.y)
else:
self._implicit_grab_motion(event.time_msec)

def _on_cursor_motion_absolute(
self, _listener: Listener, event: pointer.PointerMotionAbsoluteEvent
Expand All @@ -624,7 +694,10 @@ def _on_cursor_motion_absolute(

x, y = self.cursor.absolute_to_layout_coords(event.pointer.base, event.x, event.y)
self.cursor.move(x - self.cursor.x, y - self.cursor.y)
self._process_cursor_motion(event.time_msec, self.cursor.x, self.cursor.y)
if self._implicit_grab is None:
self._process_cursor_motion(event.time_msec, self.cursor.x, self.cursor.y)
else:
self._implicit_grab_motion(event.time_msec)

def _on_cursor_pinch_begin(
self,
Expand Down Expand Up @@ -1246,12 +1319,12 @@ def focus_window(
if keyboard := self.seat.keyboard:
self.seat.keyboard_notify_enter(surface, keyboard)

def _focus_by_click(self) -> None:
def _focus_by_click(self) -> tuple[window.WindowType, Surface | None, float, float] | None:
assert self.qtile is not None
found = self._under_pointer()

if found:
win, _, _, _ = found
win, surface, sx, sy = found

if self.exclusive_client:
# If we have a client who exclusively gets input, no other client's
Expand Down Expand Up @@ -1284,6 +1357,8 @@ def _focus_by_click(self) -> None:
if screen:
self.qtile.focus_screen(screen.index, warp=False)

return found

def _under_pointer(self) -> tuple[window.WindowType, Surface | None, float, float] | None:
"""
Find which window and surface is currently under the pointer, if any.
Expand Down

0 comments on commit 9864ed9

Please sign in to comment.