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

commit8dadee026ce33a75101e56506f784d4fdd408881
parentb537020187
authorIsaac Freund <[email protected]>
date2025-08-04 23:33
Window: store in SlotMap

This allows us to have weak references to windows that are hard to
misuse.

 build.zig               |  2 ++
 common/slotmap.zig      |  8 +++++++-
 river/Cursor.zig        |  8 ++++----
 river/Output.zig        |  2 +-
 river/Seat.zig          | 45 +++++++++++++++++++++++++++------------------
 river/Window.zig        | 33 ++++++++++++++++++---------------
 river/WindowManager.zig |  9 ++++-----
 7 files changed, 63 insertions(+), 44 deletions(-)

diff --git a/build.zig b/build.zig
index 08bfcd8..15aa910 100644
--- a/build.zig
+++ b/build.zig
@@ -148,6 +148,7 @@ pub fn build(b: *Build) !void {
 
     const flags = b.createModule(.{ .root_source_file = b.path("common/flags.zig") });
     const globber = b.createModule(.{ .root_source_file = b.path("common/globber.zig") });
+    const slotmap = b.createModule(.{ .root_source_file = b.path("common/slotmap.zig") });
 
     {
         const river = b.addExecutable(.{
@@ -175,6 +176,7 @@ pub fn build(b: *Build) !void {
         river.root_module.addImport("wlroots", wlroots);
         river.root_module.addImport("flags", flags);
         river.root_module.addImport("globber", globber);
+        river.root_module.addImport("slotmap", slotmap);
 
         river.addCSourceFile(.{
             .file = b.path("river/wlroots_log_wrapper.c"),
diff --git a/common/slotmap.zig b/common/slotmap.zig
index 4eb6ed1..9931c52 100644
--- a/common/slotmap.zig
+++ b/common/slotmap.zig
@@ -21,7 +21,8 @@ pub fn SlotMap(comptime T: type) type {
     return struct {
         const Map = @This();
 
-        pub const Key = struct {
+        /// This is packed just to make == work.
+        pub const Key = packed struct {
             generation: u32,
             index: u32,
         };
@@ -187,6 +188,11 @@ test "iteration" {
     var map: SlotMap(u64) = .empty;
     defer map.deinit(testing.allocator);
 
+    {
+        var it = map.iterator();
+        try testing.expectEqual(null, it.next());
+    }
+
     const five = try map.put(testing.allocator, 5);
     const six = try map.put(testing.allocator, 6);
     const seven = try map.put(testing.allocator, 7);
diff --git a/river/Cursor.zig b/river/Cursor.zig
index e4446df..b32f24d 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -377,13 +377,13 @@ fn updateHovered(cursor: *Cursor) void {
                         // Exclude input regions of the toplevel that extend beyond the window
                         if (result.surface != null and result.surface.?.getRootSurface() == toplevel.wlr_toplevel.base.surface) {
                             if (window.box.containsPoint(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) {
-                                cursor.seat.wm_scheduled.window = window;
+                                cursor.seat.wm_scheduled.window = window.ref;
                             }
                         } else {
-                            cursor.seat.wm_scheduled.window = window;
+                            cursor.seat.wm_scheduled.window = window.ref;
                         }
                     },
-                    .xwayland => cursor.seat.wm_scheduled.window = window,
+                    .xwayland => cursor.seat.wm_scheduled.window = window.ref,
                     .destroying => {},
                 }
             },
@@ -531,7 +531,7 @@ pub fn processAxis(cursor: *Cursor, event: *const wlr.Pointer.event.Axis) void {
 fn interact(cursor: Cursor, result: Scene.AtResult) void {
     switch (result.data) {
         .window => |window| {
-            cursor.seat.wm_scheduled.interaction = .{ .window = window };
+            cursor.seat.wm_scheduled.interaction = .{ .window = window.ref };
             server.wm.dirtyWindowing();
         },
         .shell_surface => |shell_surface| {
diff --git a/river/Output.zig b/river/Output.zig
index 2695631..5f709aa 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -276,7 +276,7 @@ pub fn manageStart(output: *Output) void {
 
             if (output.scheduled.state == .destroying) {
                 {
-                    var it = server.wm.windows.iterator(.forward);
+                    var it = server.wm.windows.iterator();
                     while (it.next()) |window| {
                         switch (window.wm_scheduled.fullscreen_requested) {
                             .fullscreen => |output_hint| {
diff --git a/river/Seat.zig b/river/Seat.zig
index 5e9bd33..fcb4853 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -80,7 +80,7 @@ pub const Event = union(enum) {
 
 pub const WmFocus = union(enum) {
     none,
-    window: *Window,
+    window: Window.Ref,
     shell_surface: *ShellSurface,
 };
 
@@ -118,7 +118,7 @@ event_queue: EventQueue = EventQueue.init(),
 /// State to be sent to the wm in the next manage sequence.
 wm_scheduled: struct {
     /// The window entered/hovered by the pointer, if any
-    window: ?*Window = null,
+    window: ?Window.Ref = null,
     /// The window clicked on, touched, etc.
     interaction: WmFocus = .none,
     op_release: bool = false,
@@ -127,7 +127,7 @@ wm_scheduled: struct {
 /// State sent to the wm in the latest manage sequence.
 wm_sent: struct {
     /// The window entered/hovered by the pointer, if any
-    window: ?*Window = null,
+    window: ?Window.Ref = null,
 } = .{},
 link_sent: wl.list.Link,
 
@@ -324,10 +324,12 @@ pub fn manageStart(seat: *Seat) void {
         }
 
         if (new) {
-            if (seat.wm_scheduled.window) |window| {
-                if (window.object) |window_v1| {
-                    seat_v1.sendPointerEnter(window_v1);
-                    seat.wm_sent.window = seat.wm_scheduled.window;
+            if (seat.wm_scheduled.window) |ref| {
+                if (ref.get()) |window| {
+                    if (window.object) |window_v1| {
+                        seat_v1.sendPointerEnter(window_v1);
+                        seat.wm_sent.window = seat.wm_scheduled.window;
+                    }
                 }
             }
         } else if (seat.wm_scheduled.window != seat.wm_sent.window) {
@@ -335,27 +337,30 @@ pub fn manageStart(seat: *Seat) void {
                 seat_v1.sendPointerLeave();
                 seat.wm_sent.window = null;
             }
-            if (seat.wm_scheduled.window) |window| {
-                if (window.object) |window_v1| {
-                    seat_v1.sendPointerEnter(window_v1);
-                    seat.wm_sent.window = window;
+            if (seat.wm_scheduled.window) |ref| {
+                if (ref.get()) |window| {
+                    if (window.object) |window_v1| {
+                        seat_v1.sendPointerEnter(window_v1);
+                        seat.wm_sent.window = seat.wm_scheduled.window;
+                    }
                 }
             }
         }
 
         switch (seat.wm_scheduled.interaction) {
             .none => {},
-            .window => |window| {
-                if (window.object) |window_v1| {
-                    seat_v1.sendWindowInteraction(window_v1);
-                    seat.wm_scheduled.interaction = .none;
+            .window => |ref| {
+                if (ref.get()) |window| {
+                    if (window.object) |window_v1| {
+                        seat_v1.sendWindowInteraction(window_v1);
+                    }
                 }
             },
             .shell_surface => |shell_surface| {
                 seat_v1.sendShellSurfaceInteraction(shell_surface.object);
-                seat.wm_scheduled.interaction = .none;
             },
         }
+        seat.wm_scheduled.interaction = .none;
 
         if (seat.op) |*op| {
             seat_v1.sendOpDelta(op.x - op.start_x, op.y - op.start_y);
@@ -436,7 +441,7 @@ fn handleRequest(
             if (!server.wm.ensureWindowing()) return;
             const data = args.window.getUserData() orelse return;
             const window: *Window = @ptrCast(@alignCast(data));
-            seat.wm_requested.focus = .{ .window = window };
+            seat.wm_requested.focus = .{ .window = window.ref };
         },
         .focus_shell_surface => |args| {
             if (!server.wm.ensureWindowing()) return;
@@ -494,7 +499,11 @@ pub fn manageFinish(seat: *Seat) void {
 
     switch (seat.wm_requested.focus) {
         .none => seat.focus(.none),
-        .window => |window| seat.focus(.{ .window = window }),
+        .window => |ref| {
+            if (ref.get()) |window| {
+                seat.focus(.{ .window = window });
+            }
+        },
         .shell_surface => |shell_surface| seat.focus(.{ .shell_surface = shell_surface }),
     }
 
diff --git a/river/Window.zig b/river/Window.zig
index ea760cd..ed71001 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -25,6 +25,7 @@ const posix = std.posix;
 const wlr = @import("wlroots");
 const wl = @import("wayland").server.wl;
 const river = @import("wayland").server.river;
+const SlotMap = @import("slotmap").SlotMap;
 
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
@@ -85,6 +86,16 @@ pub const Configure = struct {
     resizing: bool = false,
 };
 
+pub const Ref = packed struct {
+    key: SlotMap(*Window).Key,
+
+    pub fn get(ref: Ref) ?*Window {
+        return server.wm.windows.get(ref.key);
+    }
+};
+
+ref: Ref,
+
 /// The window management protocol object for this window
 /// Created in manageStart() when state is .ready
 /// Set to null in manageStart() when state is .closing
@@ -134,9 +145,6 @@ decorations_above_tree: *wlr.SceneTree,
 
 popup_tree: *wlr.SceneTree,
 
-/// WindowManager.windows
-link: wl.list.Link,
-
 /// State to be sent to the wm in the next manage sequence.
 wm_scheduled: struct {
     dimensions_hint: DimensionsHint = .{},
@@ -224,6 +232,9 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     const window = try util.gpa.create(Window);
     errdefer util.gpa.destroy(window);
 
+    const key = try server.wm.windows.put(util.gpa, window);
+    errdefer server.wm.windows.remove(key);
+
     const tree = try server.scene.hidden_tree.createSceneTree();
     errdefer tree.node.destroy();
 
@@ -231,6 +242,7 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     errdefer popup_tree.node.destroy();
 
     window.* = .{
+        .ref = .{ .key = key },
         .node = undefined,
         .impl = impl,
         .tree = tree,
@@ -247,7 +259,6 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
         .decorations_above = undefined,
         .decorations_above_tree = try tree.createSceneTree(),
         .popup_tree = popup_tree,
-        .link = undefined,
     };
 
     window.node.init(.window);
@@ -255,8 +266,6 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     window.decorations_below.init();
     window.decorations_above.init();
 
-    server.wm.windows.append(window);
-
     window.tree.node.setEnabled(false);
     window.popup_tree.node.setEnabled(false);
     window.fullscreen_background.node.setEnabled(false);
@@ -290,12 +299,6 @@ pub fn destroy(window: *Window) void {
             if (seat.focused == .window and seat.focused.window == window) {
                 seat.focus(.none);
             }
-            if (seat.wm_scheduled.window == window) {
-                seat.wm_scheduled.window = null;
-            }
-            if (seat.wm_sent.window == window) {
-                seat.wm_sent.window = null;
-            }
         }
     }
 
@@ -307,10 +310,10 @@ pub fn destroy(window: *Window) void {
     window.tree.node.destroy();
     window.popup_tree.node.destroy();
 
-    window.link.remove();
-
     window.node.deinit();
 
+    server.wm.windows.remove(window.ref.key);
+
     util.gpa.destroy(window);
 }
 
@@ -645,7 +648,7 @@ pub fn manageFinish(window: *Window) bool {
         var it = server.wm.wm_sent.seats.iterator(.forward);
         while (it.next()) |seat| {
             if (seat.wm_requested.focus == .window and
-                seat.wm_requested.focus.window == window)
+                seat.wm_requested.focus.window == window.ref)
             {
                 window.configure_scheduled.activated = true;
                 break;
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index d95776b..991ee13 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -21,6 +21,7 @@ const assert = std.debug.assert;
 const wl = @import("wayland").server.wl;
 const wlr = @import("wlroots");
 const river = @import("wayland").server.river;
+const SlotMap = @import("slotmap").SlotMap;
 
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
@@ -50,7 +51,7 @@ state: union(enum) {
     render,
 } = .idle,
 
-windows: wl.list.Head(Window, .link),
+windows: SlotMap(*Window) = .empty,
 
 /// State to be sent to the wm in the next manage sequence.
 wm_scheduled: struct {
@@ -90,7 +91,6 @@ pub fn init(wm: *WindowManager) !void {
 
     wm.* = .{
         .global = try wl.Global.create(server.wl_server, river.WindowManagerV1, 1, *WindowManager, wm, bind),
-        .windows = undefined,
         .wm_sent = .{
             .outputs = undefined,
             .seats = undefined,
@@ -100,7 +100,6 @@ pub fn init(wm: *WindowManager) !void {
         },
         .timeout = timeout,
     };
-    wm.windows.init();
     wm.wm_sent.outputs.init();
     wm.wm_sent.seats.init();
     wm.rendering_requested.list.init();
@@ -280,7 +279,7 @@ fn manageStart(wm: *WindowManager) void {
     wm.wm_scheduled.output_config = null;
 
     {
-        var it = wm.windows.safeIterator(.forward);
+        var it = wm.windows.iterator();
         while (it.next()) |window| window.manageStart();
     }
 
@@ -399,7 +398,7 @@ fn renderFinish(wm: *WindowManager) void {
     log.debug("render sequence finish", .{});
 
     {
-        var it = wm.windows.safeIterator(.forward);
+        var it = wm.windows.iterator();
         while (it.next()) |window| {
             // If a window is unmapped during a render sequence, we need to retain the saved
             // buffers until after the next manage sequence (in which the closed event will