git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit984d37a754dd919a3e6741796fa705c1cec79364
parent8163a3b774
authorIsaac Freund <[email protected]>
date2026-01-07 17:35
protocol: add river_seat_v1.pointer_warp

 protocol/river-window-management-v1.xml | 15 +++++++++++++++
 river/Cursor.zig                        |  1 -
 river/Seat.zig                          | 14 +++++++++++---
 3 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index d005671..f433a48 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1594,6 +1594,21 @@
       <arg name="x" type="int"/>
       <arg name="y" type="int"/>
     </event>
+
+    <request name="pointer_warp" since="3">
+      <description summary="warp the pointer to a given position">
+        Warp the pointer to the given position in the compositor's logical
+        coordinate space.
+
+        If the given position is outside the bounds of all outputs, the pointer
+        will be warped to the closest point inside an output instead.
+
+        This request modifies window management state and may only be made as
+        part of a manage sequence, see the river_window_manager_v1 description.
+      </description>
+      <arg name="x" type="int"/>
+      <arg name="y" type="int"/>
+    </request>
   </interface>
 
   <interface name="river_pointer_binding_v1" version="3">
diff --git a/river/Cursor.zig b/river/Cursor.zig
index b3679da..e9e91ae 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -705,7 +705,6 @@ pub fn updateState(cursor: *Cursor) void {
                 @divTrunc(now.nsec, std.time.ns_per_ms));
             cursor.passthrough(msec);
         },
-        // TODO: Leave down mode if the target surface is no longer visible.
         .ignore, .down, .op => {},
     }
 }
diff --git a/river/Seat.zig b/river/Seat.zig
index dab563d..27ad75d 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -134,6 +134,7 @@ wm_requested: struct {
         start_pointer,
         end,
     } = .none,
+    pointer_warp: ?struct { x: i32, y: i32 } = null,
 } = .{},
 
 xkb_bindings: wl.list.Head(XkbBinding, .link),
@@ -484,7 +485,6 @@ fn handleRequest(
         .destroy => {
             seat_v1.destroy();
         },
-
         .focus_window => |args| {
             if (!server.wm.ensureWindowing()) return;
             const data = args.window.getUserData() orelse return;
@@ -507,7 +507,6 @@ fn handleRequest(
             if (!server.wm.ensureWindowing()) return;
             seat.wm_requested.op = .end;
         },
-
         .get_pointer_binding => |args| {
             PointerBinding.create(
                 seat,
@@ -522,7 +521,6 @@ fn handleRequest(
                 return;
             };
         },
-
         .set_xcursor_theme => |args| {
             seat.cursor.setTheme(args.name, args.size) catch |err| switch (err) {
                 error.OutOfMemory => {
@@ -532,6 +530,10 @@ fn handleRequest(
                 },
             };
         },
+        .pointer_warp => |args| {
+            if (!server.wm.ensureWindowing()) return;
+            seat.wm_requested.pointer_warp = .{ .x = args.x, .y = args.y };
+        },
     }
 }
 
@@ -581,6 +583,12 @@ pub fn manageFinish(seat: *Seat) void {
         .end => seat.opEnd(),
     }
     seat.wm_requested.op = .none;
+
+    if (seat.wm_requested.pointer_warp) |target| {
+        seat.wm_requested.pointer_warp = null;
+        seat.cursor.wlr_cursor.warpClosest(null, @floatFromInt(target.x), @floatFromInt(target.y));
+        seat.cursor.updateState();
+    }
 }
 
 pub fn focus(seat: *Seat, new_focus: Focus) void {