Fix crash when changing focused output

With output_focus_follows_pointer=true, moving the pointer to a different
output would update Seat's "focused" output, but not the actual focused
window. If you then tried to changed focused outputs with keybinds, the
assertion in XkbBindings.focusOutput() would fail.

This commit also adds support for unfocusing a window on a pointer_leave
event.

Reported-by: Badacko
Fixes: #7
This commit is contained in:
Ben Buhse 2026-04-01 17:51:51 -05:00
commit ed72d8a15d
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
2 changed files with 30 additions and 15 deletions

View file

@ -1,7 +1,8 @@
# TODOs # TODOs
These are in rough order of my priority, though no promises I do them in this order. These are in no particular order anymore.
- [ ] Save focused window when switching back and forth between outputs via keybind
- [ ] Add a config for how to focus when a window opens with a rule on another tag; should we switch tags, add tags, or just ignore it? - [ ] Add a config for how to focus when a window opens with a rule on another tag; should we switch tags, add tags, or just ignore it?
- [ ] Support per-output single window ratio (in config; this already works at runtime) - [ ] Support per-output single window ratio (in config; this already works at runtime)
- [ ] Support per-output wallpapers - [ ] Support per-output wallpapers

View file

@ -101,6 +101,13 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *
.pointer_enter => |ev| if (seat.context.config.focus_follows_pointer) { .pointer_enter => |ev| if (seat.context.config.focus_follows_pointer) {
seat.setWindowFocus(ev.window); seat.setWindowFocus(ev.window);
}, },
.pointer_leave => if (seat.context.config.focus_follows_pointer) {
if (seat.pending_manage.window == null) {
// We only want to clear the pending focus if there's not already one set,
// e.g. from a .pointer_enter event.
seat.pending_manage.window = .clear_focus;
}
},
.window_interaction => |ev| seat.setWindowFocus(ev.window), .window_interaction => |ev| seat.setWindowFocus(ev.window),
.op_delta => |ev| { .op_delta => |ev| {
seat.pending_manage.op_delta = .{ .dx = ev.dx, .dy = ev.dy }; seat.pending_manage.op_delta = .{ .dx = ev.dx, .dy = ev.dy };
@ -111,21 +118,27 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: *
.pointer_position => |ev| { .pointer_position => |ev| {
seat.pointer_pos.x = ev.x; seat.pointer_pos.x = ev.x;
seat.pointer_pos.y = ev.y; seat.pointer_pos.y = ev.y;
if (seat.context.config.output_focus_follows_pointer) {
// Iterate over every display and check if the curser is inside it // We use this to track if the cursor is moving to a new output
if (seat.context.config.output_focus_follows_pointer) blk: {
const focused_output = seat.focused_output orelse break :blk;
if (utils.isPosInRect(seat.pointer_pos, focused_output.geometry)) {
// Same output, skip
break :blk;
}
// New output, find which one the pointer is in
var it = seat.context.wm.outputs.iterator(.forward); var it = seat.context.wm.outputs.iterator(.forward);
while (it.next()) |output| { while (it.next()) |output| {
if (utils.isPosInRect(seat.pointer_pos, output.geometry)) { if (output != focused_output and
utils.isPosInRect(seat.pointer_pos, output.geometry))
{
seat.focused_output = output; seat.focused_output = output;
break;
} }
} }
} }
}, },
else => {},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
} }
} }
@ -156,12 +169,6 @@ pub fn manage(seat: *Seat) void {
if (focused != window) { if (focused != window) {
// Tell the previously focused Window that it's no longer focused // Tell the previously focused Window that it's no longer focused
focused.pending_render.focused = false; focused.pending_render.focused = false;
// Update the Bar to have the newly-focused window's title
if (focused.output) |output| {
if (output.bar) |*bar| {
bar.pending_render.draw = true;
}
}
} }
} }
seat.focused_window = window; seat.focused_window = window;
@ -177,6 +184,13 @@ pub fn manage(seat: *Seat) void {
seat.river_seat_v1.clearFocus(); seat.river_seat_v1.clearFocus();
}, },
} }
// Bars should redraw because the title (may) have changed
var it = seat.context.wm.outputs.iterator(.forward);
while (it.next()) |output| {
if (output.bar) |*bar| {
bar.pending_render.draw = true;
}
}
} }
} }
if (seat.pending_manage.output) |pending_output| { if (seat.pending_manage.output) |pending_output| {