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

commite77149bd7b1650a3c7e429bca5450001ecf1682f
parente20ce9943d
authorIsaac Freund <[email protected]>
date2024-07-25 16:39
Window: add update sequence machinery

 river/Cursor.zig         |   3 +-
 river/Window.zig         | 176 ++++++++++++++++++++++++++++++++++++++++-------
 river/WindowManager.zig  |  17 ++++-
 river/XdgDecoration.zig  |  12 ++--
 river/XdgToplevel.zig    |  44 +++++-------
 river/XwaylandWindow.zig |  59 +++++-----------
 6 files changed, 207 insertions(+), 104 deletions(-)

diff --git a/river/Cursor.zig b/river/Cursor.zig
index 26cd3a2..e1abbce 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -331,8 +331,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
                     // If we were in down mode, we need pass along the release event
                     _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
                 },
-                .move => {},
-                .resize => |data| data.window.pending.resizing = false,
+                .move, .resize => {},
             }
 
             cursor.mode = .passthrough;
diff --git a/river/Window.zig b/river/Window.zig
index 5f806b5..d936118 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -69,17 +69,17 @@ pub const State = struct {
 };
 
 /// The window management protocol object for this window
-/// If the window is unmapped the "closed" event is sent to the current object.
-/// If the window is remapped a new object is created and sent to the window manager.
-object: ?*river.WindowV1,
+/// Created after the window is ready to be configured.
+/// Lifetime is managed through pending.state
+object: ?*river.WindowV1 = null,
 
 /// The implementation of this window
 impl: Impl,
 
 /// Link for WindowManager.windows
 link: wl.list.Link,
-/// Link for WindowManager.pending_to_wm.new_windows
-link_new: wl.list.Link,
+/// Link for WindowManager.pending.dirty_windows
+link_dirty: wl.list.Link,
 
 tree: *wlr.SceneTree,
 surface_tree: *wlr.SceneTree,
@@ -98,14 +98,35 @@ inflight_transaction: bool = false,
 /// transaction completes. See Window.destroy()
 destroying: bool = false,
 
+/// State to be sent to the window manager client in the next update sequence.
 pending: 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 an send events.
+        ready,
+        /// Indicates that the closed event will be sent in the next update sequence.
+        closing,
+    } = .init,
     box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-
     decoration_hint: river.WindowV1.DecorationHint = .only_supports_csd,
-    /// TODO output hint
-    fullscreen_requested: bool = false,
+    /// Set back to no_request at the end of each update sequence
+    fullscreen_requested: enum {
+        no_request,
+        /// TODO output hint
+        fullscreen,
+        exit,
+    } = .no_request,
+} = .{},
 
-    resizing: bool = false,
+/// State sent to the window manager client in the latest update sequence.
+/// This state is only kept around in order to avoid sending redundant events
+/// to the window manager client.
+sent: struct {
+    box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
+    decoration_hint: river.WindowV1.DecorationHint = .only_supports_csd,
 } = .{},
 
 uncommitted: struct {} = .{},
@@ -128,13 +149,6 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     const window = try util.gpa.create(Window);
     errdefer util.gpa.destroy(window);
 
-    const object = blk: {
-        const wm_v1 = server.wm.object orelse break :blk null;
-        break :blk river.WindowV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0) catch
-            return error.OutOfMemory;
-    };
-    errdefer if (object) |o| o.destroy();
-
     const tree = try server.root.hidden_tree.createSceneTree();
     errdefer tree.node.destroy();
 
@@ -142,10 +156,9 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     errdefer popup_tree.node.destroy();
 
     window.* = .{
-        .object = object,
         .impl = impl,
         .link = undefined,
-        .link_new = undefined,
+        .link_dirty = undefined,
         .tree = tree,
         .surface_tree = try tree.createSceneTree(),
         .saved_surface_tree = try tree.createSceneTree(),
@@ -163,7 +176,7 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     };
 
     server.wm.windows.prepend(window);
-    server.wm.pending.new_windows.prepend(window);
+    window.link_dirty.init();
 
     window.uncommitted_render_list_link.init();
     window.committed_render_list_link.init();
@@ -185,6 +198,7 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
 pub fn destroy(window: *Window, when: enum { lazy, assert }) void {
     assert(window.impl == .none);
     assert(!window.mapped);
+    assert(window.object == null);
 
     window.destroying = true;
 
@@ -196,7 +210,7 @@ pub fn destroy(window: *Window, when: enum { lazy, assert }) void {
         window.popup_tree.node.destroy();
 
         window.link.remove();
-        window.link_new.remove();
+        window.link_dirty.remove();
         window.uncommitted_render_list_link.remove();
         window.committed_render_list_link.remove();
         window.inflight_render_list_link.remove();
@@ -212,6 +226,121 @@ pub fn destroy(window: *Window, when: enum { lazy, assert }) void {
     }
 }
 
+fn dirtyPending(window: *Window) void {
+    switch (window.pending.state) {
+        .init => {},
+        .ready, .closing => {
+            window.link_dirty.remove();
+            server.wm.pending.dirty_windows.prepend(window);
+            server.wm.dirtyPending();
+        },
+    }
+}
+
+pub fn ready(window: *Window) void {
+    if (window.pending.state != .ready) {
+        window.pending.state = .ready;
+        window.dirtyPending();
+    }
+}
+
+pub fn closing(window: *Window) void {
+    if (window.pending.state != .closing) {
+        window.pending.state = .closing;
+        window.dirtyPending();
+    }
+}
+
+pub fn setDecorationHint(window: *Window, hint: river.WindowV1.DecorationHint) void {
+    window.pending.decoration_hint = hint;
+    if (hint != window.sent.decoration_hint) {
+        window.dirtyPending();
+    }
+}
+
+pub fn setFullscreenRequested(window: *Window, fullscreen_requested: bool) void {
+    if (fullscreen_requested) {
+        window.pending.fullscreen_requested = .fullscreen;
+    } else {
+        window.pending.fullscreen_requested = .exit;
+    }
+    window.dirtyPending();
+}
+
+/// Send dirty pending state as part of an in progress update sequence.
+pub fn sendDirty(window: *Window) !void {
+    assert(window.pending.state != .init);
+
+    switch (window.pending.state) {
+        .init => unreachable,
+        .closing => {
+            window.pending.state = .init;
+            if (window.object) |window_v1| {
+                window.object = null;
+                window_v1.sendClosed();
+                window_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+            }
+        },
+        .ready => {
+            const wm_v1 = server.wm.object.?;
+            const new = window.object == null;
+            const window_v1 = window.object orelse blk: {
+                const window_v1 = try river.WindowV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0);
+                window.object = window_v1;
+
+                window_v1.setHandler(*Window, handleRequest, null, window);
+
+                wm_v1.sendWindow(window_v1);
+                break :blk window_v1;
+            };
+            errdefer comptime unreachable;
+
+            const pending = &window.pending;
+            const sent = &window.sent;
+
+            // XXX send all dirty pending state
+            if ((pending.box.width != sent.box.width or
+                pending.box.height != sent.box.height) and !pending.box.empty())
+            {
+                window_v1.sendDimensions(window.pending.box.width, window.pending.box.height);
+                sent.box.width = pending.box.width;
+                sent.box.height = pending.box.height;
+            }
+            if (new or pending.decoration_hint != sent.decoration_hint) {
+                window_v1.sendDecorationHint(window.pending.decoration_hint);
+                sent.decoration_hint = pending.decoration_hint;
+            }
+            switch (pending.fullscreen_requested) {
+                .no_request => {},
+                .fullscreen => window_v1.sendFullscreenRequested(null),
+                .exit => window_v1.sendExitFullscreenRequested(),
+            }
+            pending.fullscreen_requested = .no_request;
+        },
+    }
+
+    window.link_dirty.remove();
+    window.link_dirty.init();
+}
+
+fn handleRequestInert(
+    window_v1: *river.WindowV1,
+    request: river.WindowV1.Request,
+    _: ?*anyopaque,
+) void {
+    if (request == .destroy) window_v1.destroy();
+}
+
+fn handleRequest(
+    window_v1: *river.WindowV1,
+    _: river.WindowV1.Request,
+    window: *Window,
+) void {
+    assert(window.object == window_v1);
+    // XXX handle requests
+    //switch (request) {}
+}
+
 /// The change in x/y position of the window during resize cannot be determined
 /// until the size of the buffer actually committed is known. Clients are permitted
 /// by the protocol to take a size smaller than that requested by the compositor in
@@ -484,7 +613,7 @@ pub fn map(window: *Window) !void {
 
     window.foreign_toplevel_handle.map();
 
-    server.wm.dirtyPending();
+    window.ready();
 }
 
 /// Called by the impl when the surface will no longer be displayed
@@ -493,15 +622,12 @@ pub fn unmap(window: *Window) void {
 
     if (!window.saved_surface_tree.node.enabled) window.saveSurfaceTree();
 
-    //window.pending_render_list_link.remove();
-    //server.root.hidden.pending.render_list.prepend(window);
-
     assert(window.mapped and !window.destroying);
     window.mapped = false;
 
     window.foreign_toplevel_handle.unmap();
 
-    server.wm.dirtyPending();
+    window.closing();
 }
 
 pub fn notifyTitle(window: *const Window) void {
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index d8d1d21..bc0aeef 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -51,7 +51,7 @@ state: union(enum) {
 pending: struct {
     /// Pending state has been modified since the last update event sent to the wm.
     dirty: bool = false,
-    new_windows: wl.list.Head(Window, .link_new),
+    dirty_windows: wl.list.Head(Window, .link_dirty),
 },
 
 /// State sent by the wm but not yet committed with a commit request.
@@ -86,7 +86,7 @@ pub fn init(wm: *WindowManager) !void {
         .global = try wl.Global.create(server.wl_server, river.WindowManagerV1, 1, *WindowManager, wm, bind),
         .windows = undefined,
         .pending = .{
-            .new_windows = undefined,
+            .dirty_windows = undefined,
         },
         .uncommitted = .{
             .render_list = undefined,
@@ -100,7 +100,7 @@ pub fn init(wm: *WindowManager) !void {
         .transaction_timeout = transaction_timeout,
     };
     wm.windows.init();
-    wm.pending.new_windows.init();
+    wm.pending.dirty_windows.init();
     wm.uncommitted.render_list.init();
     wm.committed.render_list.init();
     wm.inflight.render_list.init();
@@ -130,6 +130,7 @@ fn bind(client: *wl.Client, wm: *WindowManager, version: u32, id: u32) void {
 
     wm.object = object;
     object.setHandler(*WindowManager, handleRequest, null, wm);
+    // XXX send existing windows?
 }
 
 fn handleRequestInert(
@@ -194,6 +195,16 @@ fn sendUpdate(wm: *WindowManager) void {
 
     // XXX send all dirty pending state
 
+    {
+        var it = wm.pending.dirty_windows.safeIterator(.forward);
+        while (it.next()) |window| {
+            window.sendDirty() catch {
+                log.err("out of memory", .{});
+                continue; // Try again next update
+            };
+        }
+    }
+
     wm.pending.dirty = false;
 
     const serial = server.wl_server.nextSerial();
diff --git a/river/XdgDecoration.zig b/river/XdgDecoration.zig
index 0cd57c2..558bd10 100644
--- a/river/XdgDecoration.zig
+++ b/river/XdgDecoration.zig
@@ -75,11 +75,9 @@ fn handleRequestMode(
     const toplevel: *XdgToplevel = @ptrFromInt(decoration.wlr_decoration.toplevel.base.data);
     const window = toplevel.window;
 
-    switch (decoration.wlr_decoration.requested_mode) {
-        .none => window.pending.decoration_hint = .no_preference,
-        .client_side => window.pending.decoration_hint = .prefers_csd,
-        .server_side => window.pending.decoration_hint = .prefers_ssd,
-    }
-
-    server.wm.dirtyPending();
+    window.setDecorationHint(switch (decoration.wlr_decoration.requested_mode) {
+        .none => .no_preference,
+        .client_side => .prefers_csd,
+        .server_side => .prefers_ssd,
+    });
 }
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 0ca4a56..3b6eacc 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -64,10 +64,6 @@ map: wl.Listener(void) = wl.Listener(void).init(handleMap),
 unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
 commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
 new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
-
-// Listeners that are only active while the window is mapped
-ack_configure: wl.Listener(*wlr.XdgSurface.Configure) =
-    wl.Listener(*wlr.XdgSurface.Configure).init(handleAckConfigure),
 request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFullscreen),
 request_move: wl.Listener(*wlr.XdgToplevel.event.Move) =
     wl.Listener(*wlr.XdgToplevel.event.Move).init(handleRequestMove),
@@ -76,6 +72,10 @@ request_resize: wl.Listener(*wlr.XdgToplevel.event.Resize) =
 set_title: wl.Listener(void) = wl.Listener(void).init(handleSetTitle),
 set_app_id: wl.Listener(void) = wl.Listener(void).init(handleSetAppId),
 
+// Listeners that are only active while the window is mapped
+ack_configure: wl.Listener(*wlr.XdgSurface.Configure) =
+    wl.Listener(*wlr.XdgSurface.Configure).init(handleAckConfigure),
+
 pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void {
     const window = try Window.create(.{ .toplevel = .{
         .window = undefined,
@@ -108,6 +108,11 @@ pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void {
     wlr_toplevel.base.surface.events.map.add(&toplevel.map);
     wlr_toplevel.base.surface.events.commit.add(&toplevel.commit);
     wlr_toplevel.base.events.new_popup.add(&toplevel.new_popup);
+    wlr_toplevel.events.request_fullscreen.add(&toplevel.request_fullscreen);
+    wlr_toplevel.events.request_move.add(&toplevel.request_move);
+    wlr_toplevel.events.request_resize.add(&toplevel.request_resize);
+    wlr_toplevel.events.set_title.add(&toplevel.set_title);
+    wlr_toplevel.events.set_app_id.add(&toplevel.set_app_id);
 }
 
 /// Send a configure event, applying the inflight state of the window.
@@ -205,12 +210,17 @@ fn handleDestroy(listener: *wl.Listener(void)) void {
     }
     assert(toplevel.decoration == null);
 
-    // Remove listeners that are active for the entire lifetime of the window
+    // Remove listeners that are active for the entire lifetime of the toplevel
     toplevel.destroy.link.remove();
     toplevel.map.link.remove();
     toplevel.unmap.link.remove();
     toplevel.commit.link.remove();
     toplevel.new_popup.link.remove();
+    toplevel.request_fullscreen.link.remove();
+    toplevel.request_move.link.remove();
+    toplevel.request_resize.link.remove();
+    toplevel.set_title.link.remove();
+    toplevel.set_app_id.link.remove();
 
     // The wlr_surface may outlive the wlr_xdg_toplevel so we must clean up the user data.
     toplevel.wlr_toplevel.base.surface.data = 0;
@@ -226,14 +236,10 @@ fn handleMap(listener: *wl.Listener(void)) void {
 
     // Add listeners that are only active while mapped
     toplevel.wlr_toplevel.base.events.ack_configure.add(&toplevel.ack_configure);
-    toplevel.wlr_toplevel.events.request_fullscreen.add(&toplevel.request_fullscreen);
-    toplevel.wlr_toplevel.events.request_move.add(&toplevel.request_move);
-    toplevel.wlr_toplevel.events.request_resize.add(&toplevel.request_resize);
-    toplevel.wlr_toplevel.events.set_title.add(&toplevel.set_title);
-    toplevel.wlr_toplevel.events.set_app_id.add(&toplevel.set_app_id);
 
     toplevel.wlr_toplevel.base.getGeometry(&toplevel.geometry);
 
+    // XXX this seems like it should be deleted/moved to handleCommit()
     window.pending.box = .{
         .x = 0,
         .y = 0,
@@ -243,8 +249,6 @@ fn handleMap(listener: *wl.Listener(void)) void {
     window.inflight.box = window.pending.box;
     window.current.box = window.pending.box;
 
-    toplevel.window.pending.fullscreen_requested = toplevel.wlr_toplevel.requested.fullscreen;
-
     window.map() catch {
         log.err("out of memory", .{});
         toplevel.wlr_toplevel.resource.getClient().postNoMemory();
@@ -257,11 +261,6 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
 
     // Remove listeners that are only active while mapped
     toplevel.ack_configure.link.remove();
-    toplevel.request_fullscreen.link.remove();
-    toplevel.request_move.link.remove();
-    toplevel.request_resize.link.remove();
-    toplevel.set_title.link.remove();
-    toplevel.set_app_id.link.remove();
 
     toplevel.window.unmap();
 }
@@ -296,9 +295,7 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
     const window = toplevel.window;
 
     if (toplevel.wlr_toplevel.base.initial_commit) {
-        _ = toplevel.wlr_toplevel.setWmCapabilities(.{ .fullscreen = true });
-
-        // XXX I think this is where we actually want to send the new window event.
+        window.ready();
         return;
     }
 
@@ -389,14 +386,9 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
     }
 }
 
-/// Called when the client asks to be fullscreened. We always honor the request
-/// for now, perhaps it should be denied in some cases in the future.
 fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("request_fullscreen", listener);
-    if (toplevel.window.pending.fullscreen_requested != toplevel.wlr_toplevel.requested.fullscreen) {
-        toplevel.window.pending.fullscreen_requested = toplevel.wlr_toplevel.requested.fullscreen;
-        server.wm.dirtyPending();
-    }
+    toplevel.window.setFullscreenRequested(toplevel.wlr_toplevel.requested.fullscreen);
 }
 
 fn handleRequestMove(
diff --git a/river/XwaylandWindow.zig b/river/XwaylandWindow.zig
index e51f3f6..97a28b0 100644
--- a/river/XwaylandWindow.zig
+++ b/river/XwaylandWindow.zig
@@ -46,12 +46,6 @@ request_configure: wl.Listener(*wlr.XwaylandSurface.event.Configure) =
 set_override_redirect: wl.Listener(void) = wl.Listener(void).init(handleSetOverrideRedirect),
 associate: wl.Listener(void) = wl.Listener(void).init(handleAssociate),
 dissociate: wl.Listener(void) = wl.Listener(void).init(handleDissociate),
-
-// Active while the xwayland_surface is associated with a wlr_surface
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-
-// Active while mapped
 set_title: wl.Listener(void) = wl.Listener(void).init(handleSetTitle),
 set_class: wl.Listener(void) = wl.Listener(void).init(handleSetClass),
 set_decorations: wl.Listener(void) = wl.Listener(void).init(handleSetDecorations),
@@ -59,6 +53,10 @@ request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFull
 request_minimize: wl.Listener(*wlr.XwaylandSurface.event.Minimize) =
     wl.Listener(*wlr.XwaylandSurface.event.Minimize).init(handleRequestMinimize),
 
+// Active while the xwayland_surface is associated with a wlr_surface
+map: wl.Listener(void) = wl.Listener(void).init(handleMap),
+unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
+
 pub fn create(xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!void {
     const window = try Window.create(.{ .xwayland_window = .{
         .window = undefined,
@@ -75,6 +73,11 @@ pub fn create(xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!void {
     xwayland_surface.events.dissociate.add(&xwayland_window.dissociate);
     xwayland_surface.events.request_configure.add(&xwayland_window.request_configure);
     xwayland_surface.events.set_override_redirect.add(&xwayland_window.set_override_redirect);
+    xwayland_surface.events.set_title.add(&xwayland_window.set_title);
+    xwayland_surface.events.set_class.add(&xwayland_window.set_class);
+    xwayland_surface.events.set_decorations.add(&xwayland_window.set_decorations);
+    xwayland_surface.events.request_fullscreen.add(&xwayland_window.request_fullscreen);
+    xwayland_surface.events.request_minimize.add(&xwayland_window.request_minimize);
 
     if (xwayland_surface.surface) |surface| {
         handleAssociate(&xwayland_window.associate);
@@ -133,6 +136,11 @@ fn handleDestroy(listener: *wl.Listener(void)) void {
     xwayland_window.dissociate.link.remove();
     xwayland_window.request_configure.link.remove();
     xwayland_window.set_override_redirect.link.remove();
+    xwayland_window.set_title.link.remove();
+    xwayland_window.set_class.link.remove();
+    xwayland_window.set_decorations.link.remove();
+    xwayland_window.request_fullscreen.link.remove();
+    xwayland_window.request_minimize.link.remove();
 
     const window = xwayland_window.window;
     window.impl = .none;
@@ -160,19 +168,13 @@ pub fn handleMap(listener: *wl.Listener(void)) void {
     const surface = xwayland_surface.surface.?;
     surface.data = @intFromPtr(&window.tree.node);
 
-    // Add listeners that are only active while mapped
-    xwayland_surface.events.set_title.add(&xwayland_window.set_title);
-    xwayland_surface.events.set_class.add(&xwayland_window.set_class);
-    xwayland_surface.events.set_decorations.add(&xwayland_window.set_decorations);
-    xwayland_surface.events.request_fullscreen.add(&xwayland_window.request_fullscreen);
-    xwayland_surface.events.request_minimize.add(&xwayland_window.request_minimize);
-
     xwayland_window.surface_tree = window.surface_tree.createSceneSubsurfaceTree(surface) catch {
         log.err("out of memory", .{});
         surface.resource.getClient().postNoMemory();
         return;
     };
 
+    // XXX this seems like it should be deleted/moved to handleCommit()
     window.pending.box = .{
         .x = 0,
         .y = 0,
@@ -182,16 +184,6 @@ pub fn handleMap(listener: *wl.Listener(void)) void {
     window.inflight.box = window.pending.box;
     window.current.box = window.pending.box;
 
-    if (xwayland_surface.decorations.no_border or
-        xwayland_surface.decorations.no_title)
-    {
-        window.pending.decoration_hint = .prefers_csd;
-    } else {
-        window.pending.decoration_hint = .prefers_ssd;
-    }
-
-    window.pending.fullscreen_requested = xwayland_surface.fullscreen;
-
     window.map() catch {
         log.err("out of memory", .{});
         surface.resource.getClient().postNoMemory();
@@ -203,12 +195,6 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
 
     xwayland_window.xwayland_surface.surface.?.data = 0;
 
-    // Remove listeners that are only active while mapped
-    xwayland_window.set_title.link.remove();
-    xwayland_window.set_class.link.remove();
-    xwayland_window.request_fullscreen.link.remove();
-    xwayland_window.request_minimize.link.remove();
-
     xwayland_window.window.unmap();
 
     // Don't destroy the surface tree until after Window.unmap() has a chance
@@ -271,28 +257,19 @@ fn handleSetClass(listener: *wl.Listener(void)) void {
 
 fn handleSetDecorations(listener: *wl.Listener(void)) void {
     const xwayland_window: *XwaylandWindow = @fieldParentPtr("set_decorations", listener);
-    const window = xwayland_window.window;
 
     if (xwayland_window.xwayland_surface.decorations.no_border or
         xwayland_window.xwayland_surface.decorations.no_title)
     {
-        window.pending.decoration_hint = .prefers_csd;
+        xwayland_window.window.setDecorationHint(.prefers_csd);
     } else {
-        window.pending.decoration_hint = .prefers_ssd;
+        xwayland_window.window.setDecorationHint(.prefers_ssd);
     }
-
-    server.wm.dirtyPending();
 }
 
 fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
     const xwayland_window: *XwaylandWindow = @fieldParentPtr("request_fullscreen", listener);
-    const window = xwayland_window.window;
-    const xwayland_surface = xwayland_window.xwayland_surface;
-
-    if (window.pending.fullscreen_requested != xwayland_surface.fullscreen) {
-        window.pending.fullscreen_requested = xwayland_surface.fullscreen;
-        server.wm.dirtyPending();
-    }
+    xwayland_window.window.setFullscreenRequested(xwayland_window.xwayland_surface.fullscreen);
 }
 
 /// Some X11 clients will minimize themselves regardless of how we respond.