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

commitcfeeaddf6e46ced5f4841cfc25717afe65e93b92
parent3e6e4e0a8b
authorIsaac Freund <[email protected]>
date2024-07-29 21:39
Window: handle most rwm protocol requests

Or more precisely, update the (un)committed state in response to said
requests. It isn't actually applied yet.

 build.zig.zon           |   4 +-
 river/Window.zig        | 153 ++++++++++++++++++++++++++++++++++++++++--------
 river/WindowManager.zig |  75 +++++++++++++++---------
 river/WmNode.zig        | 126 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 306 insertions(+), 52 deletions(-)

diff --git a/build.zig.zon b/build.zig.zon
index c70ba55..892558c 100644
--- a/build.zig.zon
+++ b/build.zig.zon
@@ -8,8 +8,8 @@
             .hash = "12209db20ce873af176138b76632931def33a10539387cba745db72933c43d274d56",
         },
         .@"zig-wayland" = .{
-            .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.2.0.tar.gz",
-            .hash = "1220687c8c47a48ba285d26a05600f8700d37fc637e223ced3aa8324f3650bf52242",
+            .url = "https://codeberg.org/ifreund/zig-wayland/archive/89afa3baecf7ebfd76e3730c3381b33098f3af42.tar.gz",
+            .hash = "12206015b1750462788de094b8154eab733a36523639124ace437d4631ca01b5ba18",
         },
         .@"zig-wlroots" = .{
             .url = "https://codeberg.org/ifreund/zig-wlroots/archive/ae6151f22ceb4ccd7efb1291dea573785918a7ec.tar.gz",
diff --git a/river/Window.zig b/river/Window.zig
index d936118..1aa14a5 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -32,6 +32,7 @@ const ForeignToplevelHandle = @import("ForeignToplevelHandle.zig");
 const Output = @import("Output.zig");
 const SceneNodeData = @import("SceneNodeData.zig");
 const Seat = @import("Seat.zig");
+const WmNode = @import("WmNode.zig");
 const XdgToplevel = @import("XdgToplevel.zig");
 const XwaylandWindow = @import("XwaylandWindow.zig");
 
@@ -68,10 +69,44 @@ pub const State = struct {
     resizing: bool = false,
 };
 
+pub const WmState = struct {
+    x: i32 = 0,
+    y: i32 = 0,
+    proposed: ?struct {
+        width: u31,
+        height: u31,
+    } = null,
+    hidden: bool = false,
+    decoration_choice: enum {
+        none,
+        csd,
+        ssd,
+    } = .none,
+    border: struct {
+        edges: river.WindowV1.Edges = .{},
+        width: u31 = 0,
+        r: u32 = 0,
+        b: u32 = 0,
+        g: u32 = 0,
+        a: u32 = 0,
+    } = .{},
+    tiled: river.WindowV1.Edges = .{},
+    capabilities: river.WindowV1.Capabilities = .{
+        .window_menu = true,
+        .maximize = true,
+        .fullscreen = true,
+        .minimize = true,
+    },
+    maximized: bool = false,
+    fullscreen: bool = false, // XXX output
+    closing: bool = false,
+};
+
 /// The window management protocol object for this window
 /// Created after the window is ready to be configured.
 /// Lifetime is managed through pending.state
 object: ?*river.WindowV1 = null,
+node: WmNode,
 
 /// The implementation of this window
 impl: Impl,
@@ -91,6 +126,9 @@ popup_tree: *wlr.SceneTree,
 /// Bounds on the width/height of the window, set by the toplevel/xwayland_window implementation.
 constraints: Constraints = .{},
 
+/// 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,
 /// This is true if the Window is involved in the currently inflight transaction.
 inflight_transaction: bool = false,
@@ -129,14 +167,13 @@ sent: struct {
     decoration_hint: river.WindowV1.DecorationHint = .only_supports_csd,
 } = .{},
 
-uncommitted: struct {} = .{},
-uncommitted_render_list_link: wl.list.Link,
-
-committed: struct {} = .{},
-committed_render_list_link: wl.list.Link,
+/// State requested by the window manager client but not yet committed.
+uncommitted: WmState = .{},
+/// State requested by the window manager client and committed.
+committed: WmState = .{},
 
+/// State sent to the window as part of a transaction.
 inflight: State = .{},
-inflight_render_list_link: wl.list.Link,
 
 /// The current state represented by the scene graph.
 current: State = .{},
@@ -156,6 +193,7 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     errdefer popup_tree.node.destroy();
 
     window.* = .{
+        .node = undefined,
         .impl = impl,
         .link = undefined,
         .link_dirty = undefined,
@@ -169,19 +207,13 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
             try tree.createSceneRect(0, 0, &server.config.border_color),
         },
         .popup_tree = popup_tree,
-
-        .uncommitted_render_list_link = undefined,
-        .committed_render_list_link = undefined,
-        .inflight_render_list_link = undefined,
     };
 
+    window.node.init(.window);
+
     server.wm.windows.prepend(window);
     window.link_dirty.init();
 
-    window.uncommitted_render_list_link.init();
-    window.committed_render_list_link.init();
-    window.inflight_render_list_link.init();
-
     window.tree.node.setEnabled(false);
     window.popup_tree.node.setEnabled(false);
     window.saved_surface_tree.node.setEnabled(false);
@@ -211,11 +243,8 @@ pub fn destroy(window: *Window, when: enum { lazy, assert }) void {
 
         window.link.remove();
         window.link_dirty.remove();
-        window.uncommitted_render_list_link.remove();
-        window.committed_render_list_link.remove();
-        window.inflight_render_list_link.remove();
 
-        // XXX destroy object?
+        window.node.deinit();
 
         util.gpa.destroy(window);
     } else {
@@ -275,10 +304,24 @@ pub fn sendDirty(window: *Window) !void {
         .init => unreachable,
         .closing => {
             window.pending.state = .init;
+            window.initialized = false;
+            window.uncommitted = .{};
+            window.committed = .{};
+
+            window.node.link_uncommitted.remove();
+            window.node.link_uncommitted.init();
+            window.node.link_committed.remove();
+            window.node.link_committed.init();
+            window.node.link_inflight.remove();
+            window.node.link_inflight.init();
+
             if (window.object) |window_v1| {
                 window.object = null;
                 window_v1.sendClosed();
                 window_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+                window.node.makeInert();
+            } else {
+                assert(window.node.object == null);
             }
         },
         .ready => {
@@ -287,10 +330,11 @@ pub fn sendDirty(window: *Window) !void {
             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);
+
+                server.wm.uncommitted.render_list.append(&window.node);
+
                 break :blk window_v1;
             };
             errdefer comptime unreachable;
@@ -333,12 +377,75 @@ fn handleRequestInert(
 
 fn handleRequest(
     window_v1: *river.WindowV1,
-    _: river.WindowV1.Request,
+    request: river.WindowV1.Request,
     window: *Window,
 ) void {
     assert(window.object == window_v1);
-    // XXX handle requests
-    //switch (request) {}
+    const uncommitted = &window.uncommitted;
+    switch (request) {
+        .destroy => {}, // XXX send protocol error
+        .close => uncommitted.closing = true,
+        .get_node => |args| {
+            if (window.node.object != null) {
+                // XXX send protocol error
+            }
+            window.node.createObject(window_v1.getClient(), window_v1.getVersion(), args.id);
+        },
+        .propose_dimensions => |args| {
+            if (args.width < 0 or args.height < 0) {
+                // XXX send protocol error
+            }
+            uncommitted.proposed = .{
+                .width = @intCast(args.width),
+                .height = @intCast(args.height),
+            };
+        },
+        .hide => uncommitted.hidden = true,
+        .show => uncommitted.hidden = false,
+        .use_csd => uncommitted.decoration_choice = .csd,
+        .use_ssd => uncommitted.decoration_choice = .ssd,
+        .set_borders => |args| {
+            if (args.width < 0) {
+                // XXX send protocol error
+            }
+            uncommitted.border = .{
+                .edges = args.edges,
+                .width = @intCast(args.width),
+                .r = args.r,
+                .g = args.g,
+                .b = args.b,
+                .a = args.a,
+            };
+        },
+        .set_tiled => |args| uncommitted.tiled = args.edges,
+        .get_decoration_surface => {}, // XXX support decoration surfaces
+        .set_capabilities => |args| uncommitted.capabilities = args.caps,
+        .inform_maximized => uncommitted.maximized = true,
+        .inform_unmaximized => uncommitted.maximized = false,
+        .fullscreen => uncommitted.fullscreen = true,
+        .exit_fullscreen => uncommitted.fullscreen = false,
+    }
+}
+
+pub fn commitWmState(window: *Window) void {
+    if (!window.initialized and window.uncommitted.proposed != null) {
+        window.initialized = true;
+    }
+
+    window.committed = .{
+        .x = window.uncommitted.x,
+        .y = window.uncommitted.y,
+        .proposed = window.uncommitted.proposed orelse window.committed.proposed,
+        .hidden = window.uncommitted.hidden,
+        .decoration_choice = window.uncommitted.decoration_choice,
+        .border = window.uncommitted.border,
+        .tiled = window.uncommitted.tiled,
+        .capabilities = window.uncommitted.capabilities,
+        .maximized = window.uncommitted.maximized,
+        .fullscreen = window.uncommitted.fullscreen,
+        .closing = window.uncommitted.closing,
+    };
+    window.uncommitted.proposed = null;
 }
 
 /// The change in x/y position of the window during resize cannot be determined
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 229f86b..db752de 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -25,6 +25,7 @@ const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
 const Window = @import("Window.zig");
+const WmNode = @import("WmNode.zig");
 
 const log = std.log.scoped(.wm);
 
@@ -56,19 +57,19 @@ pending: struct {
 
 /// State sent by the wm but not yet committed with a commit request.
 uncommitted: struct {
-    render_list: wl.list.Head(Window, .uncommitted_render_list_link),
+    render_list: wl.list.Head(WmNode, .link_uncommitted),
 },
 
 /// State sent by the wm and committed with a commit request.
 committed: struct {
     dirty: bool = false,
-    render_list: wl.list.Head(Window, .committed_render_list_link),
+    render_list: wl.list.Head(WmNode, .link_committed),
 },
 
 /// State committed by the wm that has been sent to windows as part of the
 /// current transaction.
 inflight: struct {
-    render_list: wl.list.Head(Window, .inflight_render_list_link),
+    render_list: wl.list.Head(WmNode, .link_inflight),
 },
 
 dirty_idle: ?*wl.EventSource = null,
@@ -165,6 +166,16 @@ fn handleRequest(
             }
         },
         .commit => {
+            {
+                var it = wm.uncommitted.render_list.iterator(.forward);
+                while (it.next()) |node| {
+                    wm.committed.render_list.append(node);
+                    switch (node.get()) {
+                        .window => |window| window.commitWmState(),
+                    }
+                }
+            }
+
             wm.committed.dirty = true;
             switch (wm.state) {
                 .idle, .update_acked => wm.sendConfigures(),
@@ -181,21 +192,19 @@ pub fn dirtyPending(wm: *WindowManager) void {
 
     if (wm.dirty_idle == null) {
         const event_loop = server.wl_server.getEventLoop();
-        wm.dirty_idle = event_loop.addIdle(*WindowManager, handleDirty, wm) catch {
+        wm.dirty_idle = event_loop.addIdle(*WindowManager, handleDirtyPending, wm) catch {
             log.err("out of memory", .{});
             return;
         };
     }
 }
 
-fn handleDirty(wm: *WindowManager) void {
+fn handleDirtyPending(wm: *WindowManager) void {
+    assert(wm.pending.dirty);
     switch (wm.state) {
         .idle => {
             assert(!wm.committed.dirty);
-
-            if (wm.pending.dirty) {
-                wm.sendUpdate();
-            }
+            wm.sendUpdate();
         },
         .update_sent, .update_acked, .inflight_configures => {},
     }
@@ -242,19 +251,23 @@ fn sendConfigures(wm: *WindowManager) void {
 
     {
         var it = wm.inflight.render_list.iterator(.forward);
-        while (it.next()) |window| {
-            assert(!window.inflight_transaction);
-            window.inflight_transaction = true;
+        while (it.next()) |node| {
+            switch (node.get()) {
+                .window => |window| {
+                    assert(!window.inflight_transaction);
+                    window.inflight_transaction = true;
 
-            // This can happen if a window is unmapped while a layout demand including it is inflight
-            // If a window has been unmapped, don't send it a configure.
-            if (!window.mapped) continue;
+                    // This can happen if a window is unmapped while a layout demand including it
+                    // is inflight If a window has been unmapped, don't send it a configure.
+                    if (!window.mapped) continue;
 
-            if (window.configure()) {
-                wm.state.inflight_configures += 1;
+                    if (window.configure()) {
+                        wm.state.inflight_configures += 1;
 
-                window.saveSurfaceTree();
-                window.sendFrameDone();
+                        window.saveSurfaceTree();
+                        window.sendFrameDone();
+                    }
+                },
             }
         }
     }
@@ -311,11 +324,15 @@ fn commitTransaction(wm: *WindowManager) void {
 
     {
         var it = wm.inflight.render_list.iterator(.forward);
-        while (it.next()) |window| {
-            window.commitTransaction();
+        while (it.next()) |node| {
+            switch (node.get()) {
+                .window => |window| {
+                    window.commitTransaction();
 
-            window.tree.node.setEnabled(true);
-            window.popup_tree.node.setEnabled(true);
+                    window.tree.node.setEnabled(true);
+                    window.popup_tree.node.setEnabled(true);
+                },
+            }
         }
     }
 
@@ -327,9 +344,13 @@ fn commitTransaction(wm: *WindowManager) void {
     {
         // This must be done after updating cursor state in case the window was the target of move/resize.
         var it = wm.inflight.render_list.safeIterator(.forward);
-        while (it.next()) |window| {
-            window.dropSavedSurfaceTree();
-            if (window.destroying) window.destroy(.assert);
+        while (it.next()) |node| {
+            switch (node.get()) {
+                .window => |window| {
+                    window.dropSavedSurfaceTree();
+                    if (window.destroying) window.destroy(.assert);
+                },
+            }
         }
     }
 
@@ -338,6 +359,6 @@ fn commitTransaction(wm: *WindowManager) void {
     if (wm.committed.dirty) {
         wm.sendConfigures();
     } else if (wm.pending.dirty) {
-        wm.dirtyPending();
+        wm.sendUpdate();
     }
 }
diff --git a/river/WmNode.zig b/river/WmNode.zig
new file mode 100644
index 0000000..c8ccd51
--- /dev/null
+++ b/river/WmNode.zig
@@ -0,0 +1,126 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2024 The River Developers
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+const WmNode = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wl = @import("wayland").server.wl;
+const river = @import("wayland").server.river;
+
+const util = @import("util.zig");
+
+const Window = @import("Window.zig");
+
+const Type = union(enum) {
+    window: *Window,
+};
+const Tag = @typeInfo(Type).Union.tag_type.?;
+
+tag: Tag,
+object: ?*river.NodeV1 = null,
+
+/// WindowManager.uncommitted.render_list
+link_uncommitted: wl.list.Link,
+/// WindowManager.committed.render_list
+link_committed: wl.list.Link,
+/// WindowManager.inflight.render_list
+link_inflight: wl.list.Link,
+
+pub fn init(node: *WmNode, tag: Tag) void {
+    node.* = .{
+        .tag = tag,
+        .link_uncommitted = undefined,
+        .link_committed = undefined,
+        .link_inflight = undefined,
+    };
+    node.link_uncommitted.init();
+    node.link_committed.init();
+    node.link_inflight.init();
+}
+
+pub fn deinit(node: *WmNode) void {
+    assert(node.object == null);
+
+    node.link_uncommitted.remove();
+    node.link_committed.remove();
+    node.link_inflight.remove();
+}
+
+pub fn get(node: *WmNode) Type {
+    return switch (node.tag) {
+        .window => .{ .window = @fieldParentPtr("node", node) },
+    };
+}
+
+pub fn createObject(node: *WmNode, client: *wl.Client, version: u32, id: u32) void {
+    assert(node.object == null);
+    const node_v1 = river.NodeV1.create(client, version, id) catch {
+        std.log.err("out of memory", .{});
+        client.postNoMemory();
+        return;
+    };
+    node_v1.setHandler(*WmNode, handleRequest, null, node);
+    node.object = node_v1;
+}
+
+pub fn makeInert(node: *WmNode) void {
+    if (node.object) |node_v1| {
+        node_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+    }
+}
+
+fn handleRequestInert(
+    node_v1: *river.NodeV1,
+    request: river.NodeV1.Request,
+    _: ?*anyopaque,
+) void {
+    if (request == .destroy) node_v1.destroy();
+}
+
+fn handleRequest(
+    node_v1: *river.NodeV1,
+    request: river.NodeV1.Request,
+    node: *WmNode,
+) void {
+    assert(node.object == node_v1);
+    switch (request) {
+        .destroy => {
+            node_v1.destroy();
+            node.object = null;
+        },
+        .set_position => |args| switch (node.get()) {
+            .window => |window| {
+                window.uncommitted.x = args.x;
+                window.uncommitted.y = args.y;
+            },
+        },
+        .place_above => |args| {
+            const other_data = args.other.getUserData() orelse return;
+            const other: *WmNode = @ptrCast(@alignCast(other_data));
+
+            node.link_uncommitted.remove();
+            other.link_uncommitted.insert(&node.link_uncommitted);
+        },
+        .place_below => |args| {
+            const other_data = args.other.getUserData() orelse return;
+            const other: *WmNode = @ptrCast(@alignCast(other_data));
+
+            node.link_uncommitted.remove();
+            other.link_uncommitted.prev.?.insert(&node.link_uncommitted);
+        },
+    }
+}