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

commitc8bc54ac504dcbc570ac74a9d2845ff7f0e5e215
parentf6fae48936
authorIsaac Freund <[email protected]>
date2026-03-17 11:38
WindowManager: make all objects inert on wm destroy

This avoids strange/broken behavior if the active window manager
destroys the river_window_manager_v1 object but continues running and
keeps its seat/output/window objects for example.

 river/Output.zig        | 16 ++++++++++------
 river/Seat.zig          | 28 ++++++++++++++++++----------
 river/Window.zig        |  2 +-
 river/WindowManager.zig | 12 ++++++++++++
 river/XkbBindings.zig   |  6 ++++++
 5 files changed, 47 insertions(+), 17 deletions(-)

diff --git a/river/Output.zig b/river/Output.zig
index 75bb410..9d56930 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -338,12 +338,7 @@ pub fn manageStart(output: *Output) void {
             server.wm.sent.outputs.append(output);
         },
         .disabled_hard, .destroying => {
-            if (output.object) |output_v1| {
-                output_v1.sendRemoved();
-                output_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
-                output.layer_shell.makeInert();
-                handleObjectDestroy(output_v1, output);
-            }
+            output.makeInert();
 
             output.sent = output.scheduled;
 
@@ -374,6 +369,15 @@ pub fn manageStart(output: *Output) void {
     }
 }
 
+pub fn makeInert(output: *Output) void {
+    if (output.object) |output_v1| {
+        output_v1.sendRemoved();
+        output_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+        output.layer_shell.makeInert();
+        handleObjectDestroy(output_v1, output);
+    }
+}
+
 fn handleRequestInert(
     output_v1: *river.OutputV1,
     request: river.OutputV1.Request,
diff --git a/river/Seat.zig b/river/Seat.zig
index 57b7123..9bc6eda 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -277,6 +277,8 @@ pub fn create(name: [*:0]const u8) !void {
 }
 
 pub fn destroy(seat: *Seat) void {
+    seat.makeInert();
+
     while (seat.event_queue.popFront()) |event| {
         switch (event) {
             .keyboard_key => |data| data.keyboard.dropEvent(),
@@ -299,6 +301,7 @@ pub fn destroy(seat: *Seat) void {
             => {},
         }
     }
+
     {
         var it = server.input_manager.devices.iterator(.forward);
         while (it.next()) |device| {
@@ -313,9 +316,6 @@ pub fn destroy(seat: *Seat) void {
     }
     assert(seat.keyboard_groups.empty());
 
-    while (seat.xkb_bindings.first()) |binding| binding.destroy();
-    while (seat.pointer_bindings.first()) |binding| binding.destroy();
-
     seat.link.remove();
     seat.link_sent.remove();
 
@@ -381,13 +381,6 @@ pub fn processEvents(seat: *Seat) void {
 
 pub fn manageStart(seat: *Seat) void {
     if (seat.destroying) {
-        if (seat.object) |seat_v1| {
-            seat_v1.sendRemoved();
-            seat_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
-            seat.layer_shell.makeInert();
-            seat.xkb_bindings_seat.makeInert();
-            seat.object = null;
-        }
         seat.destroy();
         return;
     }
@@ -526,6 +519,21 @@ pub fn manageStart(seat: *Seat) void {
     seat.wm_scheduled.interaction = .none;
 }
 
+pub fn makeInert(seat: *Seat) void {
+    if (seat.object) |seat_v1| {
+        seat_v1.sendRemoved();
+        seat_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+
+        seat.layer_shell.makeInert();
+        seat.xkb_bindings_seat.makeInert();
+
+        while (seat.xkb_bindings.first()) |binding| binding.destroy();
+        while (seat.pointer_bindings.first()) |binding| binding.destroy();
+
+        seat.object = null;
+    }
+}
+
 fn handleRequestInert(
     seat_v1: *river.SeatV1,
     request: river.SeatV1.Request,
diff --git a/river/Window.zig b/river/Window.zig
index 8ecb8a1..408e70d 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -521,7 +521,7 @@ pub fn manageStart(window: *Window) void {
     }
 }
 
-fn makeInert(window: *Window) void {
+pub fn makeInert(window: *Window) void {
     if (window.object) |window_v1| {
         window_v1.sendClosed();
         window_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 56ac5bf..868d10a 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -137,6 +137,18 @@ fn handleRequestInert(
 fn handleDestroy(_: *river.WindowManagerV1, wm: *WindowManager) void {
     log.debug("active river_window_manager_v1 destroyed", .{});
     wm.object = null;
+    {
+        var it = server.om.outputs.iterator(.forward);
+        while (it.next()) |output| output.makeInert();
+    }
+    {
+        var it = server.input_manager.seats.iterator(.forward);
+        while (it.next()) |seat| seat.makeInert();
+    }
+    {
+        var it = wm.windows.iterator();
+        while (it.next()) |window| window.makeInert();
+    }
     switch (wm.state) {
         .idle => {},
         .inflight_configures => {},
diff --git a/river/XkbBindings.zig b/river/XkbBindings.zig
index 8f8a558..f6f18ef 100644
--- a/river/XkbBindings.zig
+++ b/river/XkbBindings.zig
@@ -51,6 +51,9 @@ fn handleRequest(
     switch (request) {
         .destroy => object.destroy(),
         .get_xkb_binding => |args| {
+            // Since we make all river_seat_v1 objects inert when the active
+            // window manager is destroyed, this check means that only the
+            // active window manager can create bindings.
             const seat_data = args.seat.getUserData() orelse return;
             const seat: *Seat = @ptrCast(@alignCast(seat_data));
             XkbBinding.create(
@@ -67,6 +70,9 @@ fn handleRequest(
             };
         },
         .get_seat => |args| {
+            // Since we make all river_seat_v1 objects inert when the active
+            // window manager is destroyed, this check means that only the
+            // active window manager can create a bindings seat.
             const seat_data = args.seat.getUserData() orelse return;
             const seat: *Seat = @ptrCast(@alignCast(seat_data));
             if (seat.xkb_bindings_seat.object != null) {