Cursor: restore previous image on unhide

If client A has an xdg_popup open and the user moves the cursor over a
surface of client B and waits for the cursor to be hidden after a
timeout, the cursor will not be shown on movement until the (invisible)
cursor is moved back into a surface of client A or somewhere the
compositor is responsible for rendering the cursor.

This is due to the (flawed) xdg popup grab interface of wlroots which
prevents wlr_seat_pointer_notify_enter() from sending events to clients
other than the one with the active xdg popup.

Closes: https://codeberg.org/river/river/issues/1192
This commit is contained in:
Isaac Freund 2025-03-16 13:37:57 +01:00
parent 543697847f
commit 8490558b8b
No known key found for this signature in database
GPG Key ID: 86DED400DDFD7A11
2 changed files with 56 additions and 16 deletions

View File

@ -108,6 +108,19 @@ const LayoutPoint = struct {
ly: f64, ly: f64,
}; };
const Image = union(enum) {
/// No cursor image
none,
/// Name of the current Xcursor shape
xcursor: [*:0]const u8,
/// Cursor surface configured by a client
client: struct {
surface: *wlr.Surface,
hotspot_x: i32,
hotspot_y: i32,
},
};
const log = std.log.scoped(.cursor); const log = std.log.scoped(.cursor);
/// Current cursor mode as well as any state needed to implement that mode /// Current cursor mode as well as any state needed to implement that mode
@ -124,9 +137,8 @@ wlr_cursor: *wlr.Cursor,
/// Xcursor manager for the currently configured Xcursor theme. /// Xcursor manager for the currently configured Xcursor theme.
xcursor_manager: *wlr.XcursorManager, xcursor_manager: *wlr.XcursorManager,
/// Name of the current Xcursor shape, or null if a client has configured a image: Image = .none,
/// surface to be used as the cursor shape instead. image_surface_destroy: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleImageSurfaceDestroy),
xcursor_name: ?[*:0]const u8 = null,
/// Number of distinct buttons currently pressed /// Number of distinct buttons currently pressed
pressed_count: u32 = 0, pressed_count: u32 = 0,
@ -286,18 +298,40 @@ pub fn setTheme(cursor: *Cursor, theme: ?[*:0]const u8, _size: ?u32) !void {
cursor.xcursor_manager.destroy(); cursor.xcursor_manager.destroy();
cursor.xcursor_manager = xcursor_manager; cursor.xcursor_manager = xcursor_manager;
if (cursor.xcursor_name) |name| { switch (cursor.image) {
cursor.setXcursor(name); .none, .client => {},
.xcursor => |name| cursor.wlr_cursor.setXcursor(xcursor_manager, name),
} }
} }
pub fn setXcursor(cursor: *Cursor, name: [*:0]const u8) void { pub fn setImage(cursor: *Cursor, image: Image) void {
cursor.wlr_cursor.setXcursor(cursor.xcursor_manager, name); switch (cursor.image) {
cursor.xcursor_name = name; .none, .xcursor => {},
.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| {
cursor.wlr_cursor.setSurface(client.surface, client.hotspot_x, client.hotspot_y);
client.surface.events.destroy.add(&cursor.image_surface_destroy);
},
}
}
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();
} }
fn clearFocus(cursor: *Cursor) void { fn clearFocus(cursor: *Cursor) void {
cursor.setXcursor("default"); cursor.setImage(.{ .xcursor = "default" });
cursor.seat.wlr_seat.pointerNotifyClearFocus(); cursor.seat.wlr_seat.pointerNotifyClearFocus();
} }
@ -740,8 +774,15 @@ fn handleRequestSetCursor(
// on the output that it's currently on and continue to do so as the // on the output that it's currently on and continue to do so as the
// cursor moves between outputs. // cursor moves between outputs.
log.debug("focused client set cursor", .{}); log.debug("focused client set cursor", .{});
cursor.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y); if (event.surface) |surface| {
cursor.xcursor_name = null; cursor.setImage(.{ .client = .{
.surface = surface,
.hotspot_x = event.hotspot_x,
.hotspot_y = event.hotspot_y,
} });
} else {
cursor.setImage(.none);
}
} }
} }
@ -757,8 +798,6 @@ pub fn hide(cursor: *Cursor) void {
cursor.hidden = true; cursor.hidden = true;
cursor.wlr_cursor.unsetImage(); cursor.wlr_cursor.unsetImage();
cursor.xcursor_name = null;
cursor.seat.wlr_seat.pointerNotifyClearFocus();
cursor.hide_cursor_timer.timerUpdate(0) catch { cursor.hide_cursor_timer.timerUpdate(0) catch {
log.err("failed to update cursor hide timeout", .{}); log.err("failed to update cursor hide timeout", .{});
}; };
@ -770,6 +809,7 @@ pub fn unhide(cursor: *Cursor) void {
}; };
if (!cursor.hidden) return; if (!cursor.hidden) return;
cursor.hidden = false; cursor.hidden = false;
cursor.setImage(cursor.image);
cursor.updateState(); cursor.updateState();
} }
@ -868,7 +908,7 @@ fn computeEdges(cursor: *const Cursor, view: *const View) wlr.Edges {
} }
} }
fn enterMode(cursor: *Cursor, mode: Mode, view: *View, xcursor_name: [*:0]const u8) void { fn enterMode(cursor: *Cursor, mode: Mode, view: *View, xcursor: [*:0]const u8) void {
assert(cursor.mode == .passthrough or cursor.mode == .down); assert(cursor.mode == .passthrough or cursor.mode == .down);
assert(mode == .move or mode == .resize); assert(mode == .move or mode == .resize);
@ -884,7 +924,7 @@ fn enterMode(cursor: *Cursor, mode: Mode, view: *View, xcursor_name: [*:0]const
} }
cursor.seat.wlr_seat.pointerNotifyClearFocus(); cursor.seat.wlr_seat.pointerNotifyClearFocus();
cursor.setXcursor(xcursor_name); cursor.setImage(.{ .xcursor = xcursor });
server.root.applyPending(); server.root.applyPending();
} }

View File

@ -519,7 +519,7 @@ fn handleRequestSetCursorShape(
// actually has pointer focus first. // actually has pointer focus first.
if (focused_client == event.seat_client) { if (focused_client == event.seat_client) {
const name = wlr.CursorShapeManagerV1.shapeName(event.shape); const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
seat.cursor.setXcursor(name); seat.cursor.setImage(.{ .xcursor = name });
} }
} }
} }