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

commitb546f080ba37643c06305d22adc0b15c0d63ef35
parent06fb8c3248
authorIsaac Freund <[email protected]>
date2025-07-04 23:35
Window: consolidate state

The separate initialized and mapped bools belong in the state enum.

 river/Window.zig        | 83 +++++++++++++++++++++++++------------------------
 river/WindowManager.zig |  2 +-
 river/XdgToplevel.zig   | 19 ++++++-----
 3 files changed, 53 insertions(+), 51 deletions(-)

diff --git a/river/Window.zig b/river/Window.zig
index 969e687..5d548de 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -80,11 +80,25 @@ pub const Configure = struct {
 };
 
 /// The window management protocol object for this window
-/// Created after the window is ready to be configured.
-/// Lifetime is managed through wm_scheduled.state
+/// Created in manageStart() when state is .ready
+/// Set to null in manageStart() when state is .closing
 object: ?*river.WindowV1 = null,
 node: WmNode,
 
+state: enum {
+    /// Initial state, also returned to after closed event is sent.
+    init,
+    /// The window is ready to be configured.
+    /// The river_window_v1 will be created in the next manage sequence.
+    ready,
+    /// The first configure has been sent but the window is not yet mapped.
+    initialized,
+    /// The window is mapped.
+    mapped,
+    /// The closed event will be sent in the next manage sequence.
+    closing,
+} = .init,
+
 /// The implementation of this window
 impl: Impl,
 
@@ -109,26 +123,11 @@ decorations_above_tree: *wlr.SceneTree,
 
 popup_tree: *wlr.SceneTree,
 
-/// Set to true once the window manager client has made its first commit
-/// proposing dimensions for a new river_window_v1 object.
-initialized: bool = false,
-mapped: bool = false,
-
 /// WindowManager.windows
 link: wl.list.Link,
 
 /// State to be sent to the wm in the next manage sequence.
 wm_scheduled: struct {
-    state: enum {
-        /// Indicates that there is currently no associated river_window_v1
-        /// object.
-        init,
-        /// Indicates that the window is ready to be configured.
-        /// Create a river_window_v1 object if needed and send events.
-        ready,
-        /// Indicates that the closed event will be sent in the next update sequence.
-        closing,
-    } = .init,
     dimensions_hint: DimensionsHint = .{},
     decoration_hint: river.WindowV1.DecorationHint = .only_supports_csd,
     /// Set back to no_request at the end of each update sequence
@@ -254,15 +253,14 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
 /// render sequence is completed as well.
 pub fn destroy(window: *Window) void {
     assert(window.impl == .destroying);
-    assert(!window.mapped);
 
-    switch (window.wm_scheduled.state) {
+    switch (window.state) {
         .init => {},
         .closing => {
             server.wm.dirtyWindowing();
             return;
         },
-        .ready => unreachable,
+        .ready, .initialized, .mapped => unreachable,
     }
     assert(window.object == null);
 
@@ -333,11 +331,10 @@ pub fn setFullscreenRequested(window: *Window, fullscreen_requested: bool) void
 
 /// Send dirty state as part of a manage sequence.
 pub fn manageStart(window: *Window) void {
-    switch (window.wm_scheduled.state) {
+    switch (window.state) {
         .init => {},
         .closing => {
-            window.initialized = false;
-            window.wm_scheduled.state = .init;
+            window.state = .init;
             window.wm_sent = .{};
             window.wm_requested = .{};
             window.rendering_sent = .{};
@@ -348,10 +345,12 @@ pub fn manageStart(window: *Window) void {
 
             window.makeInert();
         },
-        .ready => {
+        .ready, .initialized, .mapped => {
             const wm_v1 = server.wm.object orelse return;
             const new = window.object == null;
             const window_v1 = window.object orelse blk: {
+                assert(window.state == .ready);
+
                 const window_v1 = river.WindowV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0) catch {
                     log.err("out of memory", .{});
                     return; // try again next update
@@ -566,16 +565,20 @@ pub fn manageFinish(window: *Window) bool {
     // This can happen if the window is destroyed after being sent to the wm but
     // before being mapped.
     if (window.impl == .destroying) {
-        assert(window.wm_scheduled.state == .closing);
+        assert(window.state == .closing);
         return false;
     }
 
-    if (!window.initialized) {
-        if (wm_requested.dimensions != null) {
-            window.initialized = true;
-        } else {
-            return false;
-        }
+    switch (window.state) {
+        .init => unreachable,
+        .ready => {
+            if (wm_requested.dimensions == null) {
+                return false;
+            }
+            window.state = .initialized;
+        },
+        .initialized, .mapped => {},
+        .closing => return false,
     }
 
     window.configure_scheduled.ssd = wm_requested.ssd;
@@ -615,7 +618,7 @@ pub fn manageFinish(window: *Window) bool {
         .destroying => unreachable,
     };
 
-    if (track_configure and window.mapped) {
+    if (track_configure and window.state == .mapped) {
         window.surfaces.save();
         window.sendFrameDone();
     }
@@ -778,7 +781,8 @@ pub fn rootSurface(window: Window) ?*wlr.Surface {
 }
 
 pub fn sendFrameDone(window: Window) void {
-    assert(window.mapped and window.impl != .destroying);
+    assert(window.state == .mapped);
+    assert(window.impl != .destroying);
 
     var now = posix.clock_gettime(posix.CLOCK.MONOTONIC) catch @panic("CLOCK_MONOTONIC not supported");
     window.rootSurface().?.sendFrameDone(&now);
@@ -821,9 +825,9 @@ pub fn getAppId(window: Window) ?[*:0]const u8 {
 /// Called by the impl when the surface is ready to be displayed
 pub fn map(window: *Window) !void {
     log.debug("window '{?s}' mapped", .{window.getTitle()});
-
-    assert(!window.mapped and window.impl != .destroying);
-    window.mapped = true;
+    assert(window.impl != .destroying);
+    assert(window.state == .initialized);
+    window.state = .mapped;
 }
 
 /// Called by the impl when the surface will no longer be displayed
@@ -832,11 +836,10 @@ pub fn unmap(window: *Window) void {
 
     window.surfaces.save();
 
-    assert(window.mapped and window.impl != .destroying);
-    window.mapped = false;
+    assert(window.impl != .destroying);
+    assert(window.state == .mapped);
+    window.state = .closing;
 
-    assert(window.wm_scheduled.state != .closing);
-    window.wm_scheduled.state = .closing;
     server.wm.dirtyWindowing();
 }
 
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index eca05ee..2b124cd 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -409,7 +409,7 @@ fn renderFinish(wm: *WindowManager) void {
             // 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
             // be sent) for frame perfection.
-            if (window.wm_scheduled.state != .closing) {
+            if (window.state != .closing) {
                 window.surfaces.dropSaved();
             }
             if (window.impl == .destroying) {
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index f2cebe3..956f33e 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -239,17 +239,16 @@ fn handleDestroy(listener: *wl.Listener(void)) void {
 
     const window = toplevel.window;
     window.impl = .destroying;
-    switch (window.wm_scheduled.state) {
+    switch (window.state) {
         .init, .closing => {},
         // This can happen if the xdg toplevel is destroyed after the initial
-        // commit but before the window is mapped. In this case, the state is
-        // not set to .closing by handleUnmap() since handleUnmap() is not
-        // called due to the window not being mapped.
-        .ready => {
-            assert(!window.mapped);
-            window.wm_scheduled.state = .closing;
+        // commit but before the window is mapped.
+        .ready, .initialized => {
+            window.state = .closing;
             server.wm.dirtyWindowing();
         },
+        // State must have been set to closing in Window.unmap()
+        .mapped => unreachable,
     }
 }
 
@@ -306,13 +305,13 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
     });
 
     if (toplevel.wlr_toplevel.base.initial_commit) {
-        assert(window.wm_scheduled.state != .ready);
-        window.wm_scheduled.state = .ready;
+        assert(window.state != .ready);
+        window.state = .ready;
         server.wm.dirtyWindowing();
         return;
     }
 
-    if (!window.mapped) {
+    if (window.state != .mapped) {
         return;
     }