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

commit6c38726b781ceb386ed0bb70bb2255eda622f8d3
parentbb2cefd883
authorIsaac Freund <[email protected]>
date2024-07-21 17:08
WindowManager: start structuring state

 river/Cursor.zig                |   4 +-
 river/ForeignToplevelHandle.zig |  11 ++---
 river/Seat.zig                  |   7 ++-
 river/Server.zig                |   9 +---
 river/Window.zig                | 100 ++++++++++++++++++----------------------
 river/WindowManager.zig         |  58 +++++++++++++++--------
 river/XdgDecoration.zig         |  11 +++--
 river/XdgToplevel.zig           |  31 ++++---------
 river/XwaylandWindow.zig        |  31 +++++++++----
 9 files changed, 133 insertions(+), 129 deletions(-)

diff --git a/river/Cursor.zig b/river/Cursor.zig
index 5d66fea..85f49fa 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -851,7 +851,7 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
             data.delta_x = dx - @trunc(dx);
             data.delta_y = dy - @trunc(dy);
 
-            data.window.pending.move(@intFromFloat(dx), @intFromFloat(dy));
+            // XXX move window
 
             server.wm.applyPending();
         },
@@ -864,6 +864,8 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
             data.x += @intFromFloat(dx);
             data.y += @intFromFloat(dy);
 
+            if (true) return; // XXX resize window
+
             // Modify width/height of the pending box, taking constraints into account
             // The x/y coordinates of the window will be adjusted as needed in Window.resizeCommit()
             // based on the dimensions actually committed by the client.
diff --git a/river/ForeignToplevelHandle.zig b/river/ForeignToplevelHandle.zig
index 1bd5deb..fb424d3 100644
--- a/river/ForeignToplevelHandle.zig
+++ b/river/ForeignToplevelHandle.zig
@@ -90,14 +90,13 @@ fn handleForeignActivate(
 }
 
 fn handleForeignFullscreen(
-    listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen),
-    event: *wlr.ForeignToplevelHandleV1.event.Fullscreen,
+    _: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen),
+    _: *wlr.ForeignToplevelHandleV1.event.Fullscreen,
 ) void {
-    const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_fullscreen", listener);
-    const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
+    //const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_fullscreen", listener);
+    //const window: *Window = @fieldParentPtr("foreign_toplevel_handle", handle);
 
-    window.pending.fullscreen = event.fullscreen;
-    server.wm.applyPending();
+    // XXX Can I just delete this protocol?
 }
 
 fn handleForeignClose(
diff --git a/river/Seat.zig b/river/Seat.zig
index 921a86d..8426990 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -172,7 +172,7 @@ pub fn setFocusRaw(seat: *Seat, new_focus: FocusTarget) void {
     // First clear the current focus
     switch (seat.focused) {
         .window => |window| {
-            window.pending.focus -= 1;
+            //window.pending.focus -= 1; XXX update focus to send activated state
             window.destroyPopups();
         },
         .layer => |layer_surface| {
@@ -183,10 +183,9 @@ pub fn setFocusRaw(seat: *Seat, new_focus: FocusTarget) void {
 
     // Set the new focus
     switch (new_focus) {
-        .window => |target_window| {
+        .window => |_| {
             assert(server.lock_manager.state != .locked);
-            target_window.pending.focus += 1;
-            target_window.pending.urgent = false;
+            //target_window.pending.focus += 1; XXX update focus to send activated state
         },
         .layer => assert(server.lock_manager.state != .locked),
         .lock_surface => assert(server.lock_manager.state != .unlocked),
diff --git a/river/Server.zig b/river/Server.zig
index 7e15659..620c19c 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -406,17 +406,12 @@ fn handleNewXwaylandSurface(_: *wl.Listener(*wlr.XwaylandSurface), xwayland_surf
 }
 
 fn handleRequestActivate(
-    listener: *wl.Listener(*wlr.XdgActivationV1.event.RequestActivate),
+    _: *wl.Listener(*wlr.XdgActivationV1.event.RequestActivate),
     event: *wlr.XdgActivationV1.event.RequestActivate,
 ) void {
-    const server: *Server = @fieldParentPtr("request_activate", listener);
-
     const node_data = SceneNodeData.fromSurface(event.surface) orelse return;
     switch (node_data.data) {
-        .window => |window| if (window.pending.focus == 0) {
-            window.pending.urgent = true;
-            server.wm.applyPending();
-        },
+        .window => |_| {}, // XXX
         else => |tag| {
             log.info("ignoring xdg-activation-v1 activate request of {s} surface", .{@tagName(tag)});
         },
diff --git a/river/Window.zig b/river/Window.zig
index 3a327b7..0f67d8f 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -23,6 +23,7 @@ const math = std.math;
 const posix = std.posix;
 const wlr = @import("wlroots");
 const wl = @import("wayland").server.wl;
+const river = @import("wayland").server.river;
 
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
@@ -65,48 +66,20 @@ pub const State = struct {
     urgent: bool = false,
     ssd: bool = false,
     resizing: bool = false,
-
-    /// Modify the x/y of the given state by delta_x/delta_y, clamping to the
-    /// bounds of the output.
-    pub fn move(state: *State, delta_x: i32, delta_y: i32) void {
-        const border_width = if (state.ssd) server.config.border_width else 0;
-
-        const output_width = math.maxInt(i32);
-        const output_height = math.maxInt(i32);
-
-        const max_x = output_width - state.box.width - border_width;
-        state.box.x += delta_x;
-        state.box.x = @max(state.box.x, border_width);
-        state.box.x = @min(state.box.x, max_x);
-        state.box.x = @max(state.box.x, 0);
-
-        const max_y = output_height - state.box.height - border_width;
-        state.box.y += delta_y;
-        state.box.y = @max(state.box.y, border_width);
-        state.box.y = @min(state.box.y, max_y);
-        state.box.y = @max(state.box.y, 0);
-    }
-
-    pub fn clampToOutput(state: *State) void {
-        const output = state.output orelse return;
-
-        var output_width: i32 = undefined;
-        var output_height: i32 = undefined;
-        output.wlr_output.effectiveResolution(&output_width, &output_height);
-
-        const border_width = if (state.ssd) server.config.border_width else 0;
-        state.box.width = @min(state.box.width, output_width - (2 * border_width));
-        state.box.height = @min(state.box.height, output_height - (2 * border_width));
-
-        state.move(0, 0);
-    }
 };
 
+/// 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,
+
 /// The implementation of this window
 impl: Impl,
 
-/// Link for Root.windows
+/// Link for WindowManager.windows
 link: wl.list.Link,
+/// Link for WindowManager.pending_to_wm.new_windows
+link_new: wl.list.Link,
 
 tree: *wlr.SceneTree,
 surface_tree: *wlr.SceneTree,
@@ -125,20 +98,22 @@ inflight_transaction: bool = false,
 /// transaction completes. See Window.destroy()
 destroying: bool = false,
 
-/// The state of the window that is directly acted upon/modified through user input.
-///
-/// Pending state will be copied to the inflight state and communicated to clients
-/// to be applied as a single atomic transaction across all clients as soon as any
-/// in progress transaction has been completed.
-///
-/// Any time pending state is modified WindowManager.applyPending() must be called
-/// before yielding back to the event loop.
-pending: State = .{},
-pending_render_list_link: wl.list.Link,
-
-/// The state most recently sent to the layout generator and clients.
-/// This state is immutable until all clients have replied and the transaction
-/// is completed, at which point this inflight state is copied to current.
+pending: struct {
+    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,
+
+    resizing: bool = false,
+} = .{},
+
+uncommitted: struct {} = .{},
+uncommitted_render_list_link: wl.list.Link,
+
+committed: struct {} = .{},
+committed_render_list_link: wl.list.Link,
+
 inflight: State = .{},
 inflight_render_list_link: wl.list.Link,
 
@@ -153,6 +128,13 @@ 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();
 
@@ -160,8 +142,10 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     errdefer popup_tree.node.destroy();
 
     window.* = .{
+        .object = object,
         .impl = impl,
         .link = undefined,
+        .link_new = undefined,
         .tree = tree,
         .surface_tree = try tree.createSceneTree(),
         .saved_surface_tree = try tree.createSceneTree(),
@@ -173,13 +157,17 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
         },
         .popup_tree = popup_tree,
 
-        .pending_render_list_link = undefined,
+        .uncommitted_render_list_link = undefined,
+        .committed_render_list_link = undefined,
         .inflight_render_list_link = undefined,
     };
 
     server.wm.windows.prepend(window);
-    server.wm.pending.render_list.prepend(window);
-    server.wm.inflight.render_list.prepend(window);
+    server.wm.pending.new_windows.prepend(window);
+
+    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);
@@ -208,9 +196,13 @@ pub fn destroy(window: *Window, when: enum { lazy, assert }) void {
         window.popup_tree.node.destroy();
 
         window.link.remove();
-        window.pending_render_list_link.remove();
+        window.link_new.remove();
+        window.uncommitted_render_list_link.remove();
+        window.committed_render_list_link.remove();
         window.inflight_render_list_link.remove();
 
+        // XXX destroy object?
+
         util.gpa.destroy(window);
     } else {
         switch (when) {
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index bc9be75..91f9e72 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -31,18 +31,28 @@ const log = std.log.scoped(.wm);
 global: *wl.Global,
 server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
 
-/// The active window manager, if any.
-wm_v1: ?*river.WindowManagerV1 = null,
+/// The protocol object of the active window manager, if any.
+object: ?*river.WindowManagerV1 = null,
 
 windows: wl.list.Head(Window, .link),
 
-/// Pending state sent by the window manager but not yet committed
-/// TODO maybe need to split this into uncommitted and committed but not yet part of transaction
+/// Pending state from windows to be sent to the wm in the next update sequence.
 pending: struct {
-    render_list: wl.list.Head(Window, .pending_render_list_link),
+    new_windows: wl.list.Head(Window, .link_new),
 },
 
-/// Committed state from the window manager that is part of the current transaction.
+/// 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),
+},
+
+/// State sent by the wm and committed with a commit request.
+committed: struct {
+    render_list: wl.list.Head(Window, .committed_render_list_link),
+},
+
+/// 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),
 },
@@ -63,6 +73,12 @@ 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,
+        },
+        .uncommitted = .{
+            .render_list = undefined,
+        },
+        .committed = .{
             .render_list = undefined,
         },
         .inflight = .{
@@ -71,6 +87,10 @@ pub fn init(wm: *WindowManager) !void {
         .transaction_timeout = transaction_timeout,
     };
     wm.windows.init();
+    wm.pending.new_windows.init();
+    wm.uncommitted.render_list.init();
+    wm.committed.render_list.init();
+    wm.inflight.render_list.init();
 
     server.wl_server.addDestroyListener(&wm.server_destroy);
 }
@@ -83,41 +103,41 @@ fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
 }
 
 fn bind(client: *wl.Client, wm: *WindowManager, version: u32, id: u32) void {
-    const wm_v1 = river.WindowManagerV1.create(client, version, id) catch {
+    const object = river.WindowManagerV1.create(client, version, id) catch {
         client.postNoMemory();
         log.err("out of memory", .{});
         return;
     };
 
-    if (wm.wm_v1 != null) {
-        wm_v1.sendUnavailable();
-        wm_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+    if (wm.object != null) {
+        object.sendUnavailable();
+        object.setHandler(?*anyopaque, handleRequestInert, null, null);
         return;
     }
 
-    wm.wm_v1 = wm_v1;
-    wm_v1.setHandler(*WindowManager, handleRequest, null, wm);
+    wm.object = object;
+    object.setHandler(*WindowManager, handleRequest, null, wm);
 }
 
 fn handleRequestInert(
-    wm_v1: *river.WindowManagerV1,
+    object: *river.WindowManagerV1,
     request: river.WindowManagerV1.Request,
     _: ?*anyopaque,
 ) void {
-    if (request == .destroy) wm_v1.destroy();
+    if (request == .destroy) object.destroy();
 }
 
 fn handleRequest(
-    wm_v1: *river.WindowManagerV1,
+    object: *river.WindowManagerV1,
     request: river.WindowManagerV1.Request,
     wm: *WindowManager,
 ) void {
-    assert(wm.wm_v1 == wm_v1);
+    assert(wm.object == object);
     switch (request) {
         .stop => {
-            wm.wm_v1 = null;
-            wm_v1.sendFinished();
-            wm_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+            wm.object = null;
+            object.sendFinished();
+            object.setHandler(?*anyopaque, handleRequestInert, null, null);
         },
         .destroy => {
             // XXX send protocol error
diff --git a/river/XdgDecoration.zig b/river/XdgDecoration.zig
index 909eba1..840b9c2 100644
--- a/river/XdgDecoration.zig
+++ b/river/XdgDecoration.zig
@@ -75,10 +75,11 @@ fn handleRequestMode(
     const toplevel: *XdgToplevel = @ptrFromInt(decoration.wlr_decoration.toplevel.base.data);
     const window = toplevel.window;
 
-    const ssd = true;
-
-    if (window.pending.ssd != ssd) {
-        window.pending.ssd = ssd;
-        server.wm.applyPending();
+    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.applyPending();
 }
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index c70b130..427451d 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -243,7 +243,7 @@ fn handleMap(listener: *wl.Listener(void)) void {
     window.inflight.box = window.pending.box;
     window.current.box = window.pending.box;
 
-    toplevel.window.pending.fullscreen = toplevel.wlr_toplevel.requested.fullscreen;
+    toplevel.window.pending.fullscreen_requested = toplevel.wlr_toplevel.requested.fullscreen;
 
     window.map() catch {
         log.err("out of memory", .{});
@@ -298,12 +298,7 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
     if (toplevel.wlr_toplevel.base.initial_commit) {
         _ = toplevel.wlr_toplevel.setWmCapabilities(.{ .fullscreen = true });
 
-        if (toplevel.decoration) |decoration| {
-            const ssd = true;
-            _ = decoration.wlr_decoration.setMode(if (ssd) .server_side else .client_side);
-            toplevel.window.pending.ssd = ssd;
-        }
-
+        // XXX I think this is where we actually want to send the new window event.
         return;
     }
 
@@ -398,8 +393,8 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
 /// 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 != toplevel.wlr_toplevel.requested.fullscreen) {
-        toplevel.window.pending.fullscreen = toplevel.wlr_toplevel.requested.fullscreen;
+    if (toplevel.window.pending.fullscreen_requested != toplevel.wlr_toplevel.requested.fullscreen) {
+        toplevel.window.pending.fullscreen_requested = toplevel.wlr_toplevel.requested.fullscreen;
         server.wm.applyPending();
     }
 }
@@ -409,33 +404,23 @@ fn handleRequestMove(
     event: *wlr.XdgToplevel.event.Move,
 ) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("request_move", listener);
+    _ = toplevel;
     const seat: *Seat = @ptrFromInt(event.seat.seat.data);
-    const window = toplevel.window;
-
-    if (window.pending.fullscreen) return;
 
     // Moving windows with touch or tablet tool is not yet supported.
     if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
-        switch (seat.cursor.mode) {
-            .passthrough, .down => seat.cursor.startMove(window),
-            .move, .resize => {},
-        }
+        // XXX queue pointer_move_requested, applyPending()
     }
 }
 
 fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), event: *wlr.XdgToplevel.event.Resize) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("request_resize", listener);
+    _ = toplevel;
     const seat: *Seat = @ptrFromInt(event.seat.seat.data);
-    const window = toplevel.window;
-
-    if (window.pending.fullscreen) return;
 
     // Resizing windows with touch or tablet tool is not yet supported.
     if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
-        switch (seat.cursor.mode) {
-            .passthrough, .down => seat.cursor.startResize(window, event.edges),
-            .move, .resize => {},
-        }
+        // XXX queue pointer_resize_requested, applyPending()
     }
 }
 
diff --git a/river/XwaylandWindow.zig b/river/XwaylandWindow.zig
index 7e1e487..a234534 100644
--- a/river/XwaylandWindow.zig
+++ b/river/XwaylandWindow.zig
@@ -182,10 +182,15 @@ pub fn handleMap(listener: *wl.Listener(void)) void {
     window.inflight.box = window.pending.box;
     window.current.box = window.pending.box;
 
-    // This will be overwritten in Window.map() if the window is matched by a rule.
-    window.pending.ssd = !xwayland_surface.decorations.no_border;
+    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 = xwayland_surface.fullscreen;
+    window.pending.fullscreen_requested = xwayland_surface.fullscreen;
 
     window.map() catch {
         log.err("out of memory", .{});
@@ -268,18 +273,24 @@ fn handleSetDecorations(listener: *wl.Listener(void)) void {
     const xwayland_window: *XwaylandWindow = @fieldParentPtr("set_decorations", listener);
     const window = xwayland_window.window;
 
-    const ssd = !xwayland_window.xwayland_surface.decorations.no_border;
-
-    if (window.pending.ssd != ssd) {
-        window.pending.ssd = ssd;
-        server.wm.applyPending();
+    if (xwayland_window.xwayland_surface.decorations.no_border or
+        xwayland_window.xwayland_surface.decorations.no_title)
+    {
+        window.pending.decoration_hint = .prefers_csd;
+    } else {
+        window.pending.decoration_hint = .prefers_ssd;
     }
+
+    server.wm.applyPending();
 }
 
 fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
     const xwayland_window: *XwaylandWindow = @fieldParentPtr("request_fullscreen", listener);
-    if (xwayland_window.window.pending.fullscreen != xwayland_window.xwayland_surface.fullscreen) {
-        xwayland_window.window.pending.fullscreen = xwayland_window.xwayland_surface.fullscreen;
+    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.applyPending();
     }
 }