Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
protocol: allow wm to set pointer cursor
This seems to be the simplest way to give window managers control over
the cursor. It's quite nice to avoid a direct dependency on the
cursor-shape-v1 protocol and also avoid copy-pasting the shape enum.
We can add a river_seat_v1.sync_next_pointer_cursor_change request to
synchronize this with rendering state in the future if deemed necessary.
protocol/river-window-management-v1.xml | 17 +++++
river/Cursor.zig | 109 ++++++++++++++++++++++++++------
river/Server.zig | 24 ++++---
3 files changed, 122 insertions(+), 28 deletions(-)
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 9ea4539..72a9931 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1390,6 +1390,18 @@
high-level information about pointer input, define pointer bindings, etc.
For keyboard bindings, see the river-xkb-bindings-v1 protocol.
+
+ Since version 4: The cursor surface/shape set by the window manager on the
+ wl_pointer of this seat is used when no client has pointer focus, for
+ example during a pointer operation. Since the window manager is allowed to
+ set cursor surface/shape even when it does not have pointer focus, the
+ compositor must ignore the serial argument of wl_pointer.set_cursor and
+ wp_cursor_shape_device_v1.set_shape requests made by the window manager.
+
+ The most recent cursor surface/shape set by the window manager is
+ remembered by the compositor and restored whenever no client has pointer
+ focus. If the window manager never sets a cursor surface/shape, the
+ "default" shape is used.
</description>
<request name="destroy" type="destructor">
@@ -1565,6 +1577,11 @@
This request is ignored if an operation is already in progress.
+ The compositor must ensure that no client has pointer focus during the
+ pointer operation. This means that the window manager has control
+ over the pointer's cursor surface/shape during the pointer operation.
+ See the river_seat_v1 description.
+
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>
diff --git a/river/Cursor.zig b/river/Cursor.zig
index e7b897c..0266771 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -65,6 +65,19 @@ const Mode = union(enum) {
const default_size = 24;
+const Image = union(enum) {
+ /// No cursor image
+ none,
+ /// Name of the current Xcursor shape
+ xcursor: [*:0]const u8,
+ /// Cursor surface configured by the client
+ client: struct {
+ surface: *wlr.Surface,
+ hotspot_x: i32,
+ hotspot_y: i32,
+ },
+};
+
const LayoutPoint = struct {
lx: f64,
ly: f64,
@@ -78,9 +91,12 @@ wlr_cursor: *wlr.Cursor,
/// Xcursor manager for the currently configured Xcursor theme.
xcursor_manager: *wlr.XcursorManager,
-/// Name of the current Xcursor shape, or null if a client has configured a
-/// surface to be used as the cursor shape instead.
-xcursor_name: ?[*:0]const u8 = null,
+/// The currently rendered cursor image
+image: Image = .none,
+image_surface_destroy: wl.Listener(*wlr.Surface) = .init(handleImageSurfaceDestroy),
+/// The most recent cursor image set by the window manager client
+wm_image: Image = .{ .xcursor = "default" },
+wm_image_surface_destroy: wl.Listener(*wlr.Surface) = .init(handleWmImageSurfaceDestroy),
/// The set of currently pressed pointer buttons and the corresponding pointer mapping if any.
pressed: std.AutoHashMapUnmanaged(u32, ?*PointerBinding) = .{},
@@ -231,39 +247,94 @@ pub fn setTheme(cursor: *Cursor, theme: ?[*:0]const u8, _size: ?u32) !void {
cursor.xcursor_manager.destroy();
cursor.xcursor_manager = xcursor_manager;
- if (cursor.xcursor_name) |name| {
- cursor.setXcursor(name);
+ switch (cursor.image) {
+ .none, .client => {},
+ .xcursor => |name| cursor.wlr_cursor.setXcursor(xcursor_manager, name),
+ }
+}
+
+pub fn setImage(cursor: *Cursor, image: Image) void {
+ if (cursor.image == .client) {
+ cursor.image_surface_destroy.link.remove();
+ }
+ cursor.image = image;
+ switch (cursor.image) {
+ .none => cursor.wlr_cursor.unsetImage(),
+ .xcursor => |name| cursor.wlr_cursor.setXcursor(cursor.xcursor_manager, name),
+ .client => |client| {
+ client.surface.events.destroy.add(&cursor.image_surface_destroy);
+ cursor.wlr_cursor.setSurface(client.surface, client.hotspot_x, client.hotspot_y);
+ },
+ }
+}
+
+fn handleImageSurfaceDestroy(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
+ const cursor: *Cursor = @fieldParentPtr("image_surface_destroy", listener);
+ // wlroots calls wlr_cursor_unset_image() automatically
+ // when the cursor surface is destroyed.
+ cursor.image = .none;
+ cursor.image_surface_destroy.link.remove();
+}
+
+pub fn setWmImage(cursor: *Cursor, wm_image: Image) void {
+ if (cursor.wm_image == .client) {
+ cursor.wm_image_surface_destroy.link.remove();
+ }
+ cursor.wm_image = wm_image;
+ if (cursor.wm_image == .client) {
+ cursor.wm_image.client.surface.events.destroy.add(&cursor.wm_image_surface_destroy);
+ }
+ if (cursor.seat.wlr_seat.pointer_state.focused_client == null) {
+ cursor.setImage(wm_image);
}
}
-pub fn setXcursor(cursor: *Cursor, name: [*:0]const u8) void {
- cursor.wlr_cursor.setXcursor(cursor.xcursor_manager, name);
- cursor.xcursor_name = name;
+fn handleWmImageSurfaceDestroy(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
+ const cursor: *Cursor = @fieldParentPtr("wm_image_surface_destroy", listener);
+ cursor.wm_image = .none;
+ cursor.wm_image_surface_destroy.link.remove();
}
fn handleRequestSetCursor(
listener: *wl.Listener(*wlr.Seat.event.RequestSetCursor),
event: *wlr.Seat.event.RequestSetCursor,
) void {
- // This event is rasied by the seat when a client provides a cursor image
const cursor: *Cursor = @fieldParentPtr("request_set_cursor", listener);
const focused_client = cursor.seat.wlr_seat.pointer_state.focused_client;
- // This can be sent by any client, so we check to make sure this one is
- // actually has pointer focus first.
- if (focused_client == event.seat_client) {
- // Once we've vetted the client, we can tell the cursor to use the
- // provided surface as the cursor image. It will set the hardware cursor
- // on the output that it's currently on and continue to do so as the
- // cursor moves between outputs.
+ // Only the client with pointer focus is allowed to set the cursor
+ if (event.seat_client == focused_client) {
log.debug("focused client set cursor", .{});
- cursor.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y);
- cursor.xcursor_name = null;
+ if (event.surface) |surface| {
+ cursor.setImage(.{ .client = .{
+ .surface = surface,
+ .hotspot_x = event.hotspot_x,
+ .hotspot_y = event.hotspot_y,
+ } });
+ } else {
+ cursor.setImage(.none);
+ }
+ }
+ // Except for the window manager client
+ if (server.wm.object) |object| {
+ if (event.seat_client.client == object.getClient() and
+ object.getVersion() >= 4)
+ {
+ if (event.surface) |surface| {
+ cursor.setWmImage(.{ .client = .{
+ .surface = surface,
+ .hotspot_x = event.hotspot_x,
+ .hotspot_y = event.hotspot_y,
+ } });
+ } else {
+ cursor.setWmImage(.none);
+ }
+ }
}
}
fn clearFocus(cursor: *Cursor) void {
- cursor.setXcursor("default");
+ cursor.setImage(cursor.wm_image);
cursor.seat.wlr_seat.pointerNotifyClearFocus();
}
diff --git a/river/Server.zig b/river/Server.zig
index 2053e6e..fc5845d 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -472,30 +472,36 @@ fn handleRequestActivate(
}
fn handleRequestSetCursorShape(
- _: *wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape),
+ listener: *wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape),
event: *wlr.CursorShapeManagerV1.event.RequestSetShape,
) void {
+ const server: *Server = @fieldParentPtr("request_set_cursor_shape", listener);
const seat: *Seat = @ptrCast(@alignCast(event.seat_client.seat.data));
+ const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
+
if (event.tablet_tool) |wp_tool| {
assert(event.device_type == .tablet_tool);
const tool = TabletTool.get(event.seat_client.seat, wp_tool.wlr_tool) catch return;
-
if (tool.allowSetCursor(event.seat_client, event.serial)) {
- const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
tool.wlr_cursor.setXcursor(seat.cursor.xcursor_manager, name);
}
} else {
assert(event.device_type == .pointer);
+ // Only the client with pointer focus is allowed to set the cursor
const focused_client = event.seat_client.seat.pointer_state.focused_client;
-
- // This can be sent by any client, so we check to make sure this one is
- // actually has pointer focus first.
- if (focused_client == event.seat_client) {
- const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
- seat.cursor.setXcursor(name);
+ if (event.seat_client == focused_client) {
+ seat.cursor.setImage(.{ .xcursor = name });
+ }
+ // Except for the window manager client
+ if (server.wm.object) |object| {
+ if (event.seat_client.client == object.getClient() and
+ object.getVersion() >= 4)
+ {
+ seat.cursor.setWmImage(.{ .xcursor = name });
+ }
}
}
}