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

commit4fcedc5da567fd6fb99816c416df1d5beb4795f9
parent3c9c4b1cba
authorIsaac Freund <[email protected]>
date2026-03-03 14:33
layer-shell: send focus_none more consistently

Currently river does not send river_layer_shell_seat_v1.focus_none
when a layer surface that currently has non-exclusive focus is closed or
otherwise looses focus (e.g. because the window manager requested a
window to be focused or cleared focus).

This status quo behavior is undesirable since:

1. It does not comply to the letter of the protocol specification
2. It requires window managers to do more work in practice

With this patch, the focus_none event is sent in the next manage
sequence whenever there is a state transition from a layer surface
having focus to no layer surface having focus.

 protocol/river-layer-shell-v1.xml |  4 ++++
 river/LayerShell.zig              |  2 +-
 river/LayerShellSeat.zig          |  3 ---
 river/LayerSurface.zig            | 11 +++++++---
 river/Seat.zig                    | 43 +++++++++++++++++++++++----------------
 5 files changed, 38 insertions(+), 25 deletions(-)

diff --git a/protocol/river-layer-shell-v1.xml b/protocol/river-layer-shell-v1.xml
index 7e45cb0..0167e9d 100644
--- a/protocol/river-layer-shell-v1.xml
+++ b/protocol/river-layer-shell-v1.xml
@@ -168,6 +168,10 @@
         focus during the same manage sequence in which this event is sent, the
         layer surface will not be focused.
 
+        If the layer surface with non-exclusive focus is closed or the window
+        manager chooses to move focus away from the layer surface, a focus_none
+        event will be sent in the next manage sequence.
+
         This event will be followed by a manage_start event after all other new
         state has been sent by the server.
       </description>
diff --git a/river/LayerShell.zig b/river/LayerShell.zig
index 681bc04..6578f1f 100644
--- a/river/LayerShell.zig
+++ b/river/LayerShell.zig
@@ -159,7 +159,7 @@ fn handleNewSurface(_: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wl
     };
 }
 
-pub fn updateFocus(_: *LayerShell) void {
+pub fn checkExclusiveFocus(_: *LayerShell) void {
     // Find the topmost layer surface (if any) in the top or overlay layers which
     // requests exclusive keyboard interactivity.
     const to_focus = blk: {
diff --git a/river/LayerShellSeat.zig b/river/LayerShellSeat.zig
index 9c64fb7..767f759 100644
--- a/river/LayerShellSeat.zig
+++ b/river/LayerShellSeat.zig
@@ -90,7 +90,4 @@ pub fn manageStart(shell_seat: *LayerShellSeat) void {
         }
     }
     shell_seat.sent.focus = shell_seat.scheduled.focus;
-    if (shell_seat.scheduled.focus == .non_exclusive) {
-        shell_seat.scheduled.focus = .none;
-    }
 }
diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig
index c8af056..e8557f5 100644
--- a/river/LayerSurface.zig
+++ b/river/LayerSurface.zig
@@ -113,7 +113,7 @@ fn handleMap(listener: *wl.Listener(void)) void {
     // Beware: it is possible for arrange() to destroy this LayerSurface!
     const output: *Output = @ptrCast(@alignCast(layer_surface.wlr_layer_surface.output.?.data));
     output.layer_shell.arrange();
-    server.layer_shell.updateFocus();
+    server.layer_shell.checkExclusiveFocus();
     server.wm.dirtyWindowing();
 }
 
@@ -128,13 +128,18 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
             if (seat.focused == .layer_surface and seat.focused.layer_surface == layer_surface) {
                 seat.focus(.none);
             }
+            if (seat.layer_shell.scheduled.focus == .non_exclusive and
+                seat.layer_shell.scheduled.focus.non_exclusive == layer_surface.ref)
+            {
+                seat.layer_shell.scheduled.focus = .none;
+            }
         }
     }
 
     // Beware: it is possible for arrange() to destroy this LayerSurface!
     const output: *Output = @ptrCast(@alignCast(layer_surface.wlr_layer_surface.output.?.data));
     output.layer_shell.arrange();
-    server.layer_shell.updateFocus();
+    server.layer_shell.checkExclusiveFocus();
     server.wm.dirtyWindowing();
 }
 
@@ -156,7 +161,7 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
         // Beware: it is possible for arrange() to destroy this LayerSurface!
         const output: *Output = @ptrCast(@alignCast(layer_surface.wlr_layer_surface.output.?.data));
         output.layer_shell.arrange();
-        server.layer_shell.updateFocus();
+        server.layer_shell.checkExclusiveFocus();
         server.wm.dirtyWindowing();
     }
 }
diff --git a/river/Seat.zig b/river/Seat.zig
index 5a0f12a..4077e9a 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -558,24 +558,31 @@ pub fn manageFinish(seat: *Seat) void {
         .exclusive => |ref| if (ref.get()) |layer_surface| {
             seat.focus(.{ .layer_surface = layer_surface });
         },
-        .non_exclusive, .none => switch (seat.wm_requested.focus) {
-            .none => switch (seat.layer_shell.sent.focus) {
-                .exclusive => unreachable,
-                .non_exclusive => |ref| if (ref.get()) |layer_surface| {
-                    seat.focus(.{ .layer_surface = layer_surface });
-                    seat.layer_shell.sent.focus = .none;
-                },
-                .none => {},
-            },
-            .clear => {
-                seat.focus(.none);
-            },
-            .window => |ref| if (ref.get()) |window| {
-                seat.focus(.{ .window = window });
-            },
-            .shell_surface => |shell_surface| {
-                seat.focus(.{ .shell_surface = shell_surface });
-            },
+        .non_exclusive, .none => {
+            if (seat.wm_requested.focus == .none) {
+                switch (seat.layer_shell.sent.focus) {
+                    .exclusive => unreachable,
+                    .non_exclusive => |ref| if (ref.get()) |layer_surface| {
+                        seat.focus(.{ .layer_surface = layer_surface });
+                    },
+                    .none => {},
+                }
+            } else {
+                switch (seat.wm_requested.focus) {
+                    .none => unreachable,
+                    .clear => seat.focus(.none),
+                    .window => |ref| if (ref.get()) |window| seat.focus(.{ .window = window }),
+                    .shell_surface => |shell_surface| seat.focus(.{ .shell_surface = shell_surface }),
+                }
+                switch (seat.layer_shell.sent.focus) {
+                    .exclusive => unreachable,
+                    .non_exclusive => {
+                        seat.layer_shell.scheduled.focus = .none;
+                        server.wm.dirtyWindowing();
+                    },
+                    .none => {},
+                }
+            }
         },
     }
     seat.wm_requested.focus = .none;