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

commit39c7dba952c9988ff246688bb78b67080318b423
parent7b47e95f4a
authorIsaac Freund <[email protected]>
date2024-07-31 13:54
WindowManager: send configures

 protocol/river-window-management-v1.xml |  3 ++
 river/Window.zig                        | 81 +++++++++++++++++++++------------
 river/WindowManager.zig                 | 18 ++------
 river/XdgToplevel.zig                   | 63 +++++++++++++++++--------
 river/XwaylandWindow.zig                | 49 ++++++++++++--------
 5 files changed, 134 insertions(+), 80 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 58f5b9d..d990c53 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -387,6 +387,9 @@
         Tell the client to use client side decoration and draw its own title
         bar, borders, etc.
 
+        This is the default if neither this request nor the use_ssd request is
+        ever made.
+
         This request is double-buffered state and will not be applied until the
         next river_window_manager_v1.commit request.
       </description>
diff --git a/river/Window.zig b/river/Window.zig
index f3bcdeb..1c10342 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -56,16 +56,27 @@ const Impl = union(enum) {
     none,
 };
 
+pub const Border = struct {
+    edges: river.WindowV1.Edges = .{},
+    width: u31 = 0,
+    r: u32 = 0,
+    b: u32 = 0,
+    g: u32 = 0,
+    a: u32 = 0,
+};
+
 pub const State = struct {
     /// The output-relative coordinates of the window and dimensions requested by river.
     box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-
+    hidden: bool = false,
     /// Number of seats currently focusing the window
     focus: u32 = 0,
-
-    fullscreen: bool = false,
-    urgent: bool = false,
     ssd: bool = false,
+    border: Border = .{},
+    tiled: river.WindowV1.Edges = .{},
+    capabilities: river.WindowV1.Capabilities = .{},
+    maximized: bool = false,
+    fullscreen: bool = false,
     resizing: bool = false,
 };
 
@@ -77,19 +88,8 @@ pub const WmState = struct {
         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,
-    } = .{},
+    ssd: bool = false,
+    border: Border = .{},
     tiled: river.WindowV1.Edges = .{},
     capabilities: river.WindowV1.Capabilities = .{
         .window_menu = true,
@@ -130,8 +130,6 @@ constraints: Constraints = .{},
 /// 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,
 /// This indicates that the window should be destroyed when the current
 /// transaction completes. See Window.destroy()
 destroying: bool = false,
@@ -402,8 +400,8 @@ fn handleRequest(
         },
         .hide => uncommitted.hidden = true,
         .show => uncommitted.hidden = false,
-        .use_csd => uncommitted.decoration_choice = .csd,
-        .use_ssd => uncommitted.decoration_choice = .ssd,
+        .use_ssd => uncommitted.ssd = true,
+        .use_csd => uncommitted.ssd = false,
         .set_borders => |args| {
             if (args.width < 0) {
                 // XXX send protocol error
@@ -437,7 +435,7 @@ pub fn commitWmState(window: *Window) void {
         .y = window.uncommitted.y,
         .proposed = window.uncommitted.proposed orelse window.committed.proposed,
         .hidden = window.uncommitted.hidden,
-        .decoration_choice = window.uncommitted.decoration_choice,
+        .ssd = window.uncommitted.ssd,
         .border = window.uncommitted.border,
         .tiled = window.uncommitted.tiled,
         .capabilities = window.uncommitted.capabilities,
@@ -481,9 +479,6 @@ pub fn resizeUpdatePosition(window: *Window, width: i32, height: i32) void {
 }
 
 pub fn commitTransaction(window: *Window) void {
-    assert(window.inflight_transaction);
-    window.inflight_transaction = false;
-
     window.foreign_toplevel_handle.update();
 
     window.dropSavedSurfaceTree();
@@ -604,14 +599,44 @@ pub fn updateSceneState(window: *Window) void {
     }
 }
 
+/// Applies committed state from the window manager client to the inflight state.
 /// Returns true if the configure should be waited for by the transaction system.
 pub fn configure(window: *Window) bool {
+    if (!window.initialized) return false;
+
     assert(window.mapped and !window.destroying);
-    switch (window.impl) {
-        .toplevel => |*toplevel| return toplevel.configure(),
-        .xwayland => |*xwindow| return xwindow.configure(),
+
+    const committed = &window.committed;
+    window.inflight = .{
+        .box = .{
+            .x = committed.x,
+            .y = committed.y,
+            .width = if (committed.proposed) |p| p.width else window.pending.box.width,
+            .height = if (committed.proposed) |p| p.height else window.pending.box.height,
+        },
+        .hidden = committed.hidden,
+        .focus = 0, // XXX
+        .ssd = committed.ssd,
+        .border = committed.border,
+        .tiled = committed.tiled,
+        .capabilities = committed.capabilities,
+        .maximized = committed.maximized,
+        .fullscreen = committed.fullscreen,
+        .resizing = false, // XXX
+    };
+
+    const track_configure = switch (window.impl) {
+        .toplevel => |*toplevel| toplevel.configure(),
+        .xwayland => |*xwindow| xwindow.configure(),
         .none => unreachable,
+    };
+
+    if (track_configure) {
+        window.saveSurfaceTree();
+        window.sendFrameDone();
     }
+
+    return track_configure;
 }
 
 /// Returns null if the window is currently being destroyed and no longer has
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index db752de..acb2326 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -169,6 +169,7 @@ fn handleRequest(
             {
                 var it = wm.uncommitted.render_list.iterator(.forward);
                 while (it.next()) |node| {
+                    node.link_committed.remove();
                     wm.committed.render_list.append(node);
                     switch (node.get()) {
                         .window => |window| window.commitWmState(),
@@ -243,29 +244,18 @@ fn sendConfigures(wm: *WindowManager) void {
         .update_sent, .inflight_configures => unreachable,
     }
     assert(wm.committed.dirty);
-
-    // XXX apply committed to inflight
     wm.committed.dirty = false;
 
     wm.state = .{ .inflight_configures = 0 };
-
     {
-        var it = wm.inflight.render_list.iterator(.forward);
+        var it = wm.committed.render_list.iterator(.forward);
         while (it.next()) |node| {
+            node.link_inflight.remove();
+            wm.inflight.render_list.append(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;
-
                     if (window.configure()) {
                         wm.state.inflight_configures += 1;
-
-                        window.saveSurfaceTree();
-                        window.sendFrameDone();
                     }
                 },
             }
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 3b6eacc..7025dca 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -130,16 +130,7 @@ pub fn configure(toplevel: *XdgToplevel) bool {
     const inflight = &toplevel.window.inflight;
     const current = &toplevel.window.current;
 
-    // We avoid a special case for newly mapped windows which we have not yet
-    // configured by setting the current width/height to the initial width/height
-    // of the window in handleMap().
-    if (inflight.box.width == current.box.width and
-        inflight.box.height == current.box.height and
-        (inflight.focus != 0) == (current.focus != 0) and
-        inflight.fullscreen == current.fullscreen and
-        inflight.ssd == current.ssd and
-        inflight.resizing == current.resizing)
-    {
+    if (!toplevel.needsConfigure()) {
         // If no new configure is required, continue to track a timed out configure
         // from the previous transaction if any.
         switch (toplevel.configure_state) {
@@ -159,30 +150,36 @@ pub fn configure(toplevel: *XdgToplevel) bool {
     const wlr_toplevel = toplevel.wlr_toplevel;
 
     _ = wlr_toplevel.setActivated(inflight.focus != 0);
+    _ = wlr_toplevel.setTiled(.{
+        .top = inflight.tiled.top,
+        .bottom = inflight.tiled.bottom,
+        .left = inflight.tiled.left,
+        .right = inflight.tiled.right,
+    });
+    _ = wlr_toplevel.setWmCapabilities(.{
+        .window_menu = inflight.capabilities.window_menu,
+        .maximize = inflight.capabilities.maximize,
+        .fullscreen = inflight.capabilities.fullscreen,
+        .minimize = inflight.capabilities.minimize,
+    });
+    _ = wlr_toplevel.setMaximized(inflight.maximized);
     _ = wlr_toplevel.setFullscreen(inflight.fullscreen);
     _ = wlr_toplevel.setResizing(inflight.resizing);
 
-    // TODO
-    if (true) {
-        _ = wlr_toplevel.setTiled(.{ .top = false, .bottom = false, .left = false, .right = false });
-    } else {
-        _ = wlr_toplevel.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
-    }
-
     if (toplevel.decoration) |decoration| {
         _ = decoration.wlr_decoration.setMode(if (inflight.ssd) .server_side else .client_side);
     }
 
     // We need to call this wlroots function even if the inflight dimensions
     // match the current dimensions in order to prevent wlroots internal state
-    // from getting out of sync in the case where a client has resized ittoplevel.
+    // from getting out of sync in the case where a client has resized the toplevel.
     const configure_serial = wlr_toplevel.setSize(inflight.box.width, inflight.box.height);
 
     // Only track configures with the transaction system if they affect the dimensions of the window.
     // If the configure state is not idle this means we are currently tracking a timed out
     // configure from a previous transaction and should instead track the newly sent configure.
-    if (inflight.box.width == current.box.width and
-        inflight.box.height == current.box.height and
+    if (inflight.box.width != 0 and inflight.box.width == current.box.width and
+        inflight.box.height != 0 and inflight.box.height == current.box.height and
         toplevel.configure_state == .idle)
     {
         return false;
@@ -195,6 +192,32 @@ pub fn configure(toplevel: *XdgToplevel) bool {
     return true;
 }
 
+fn needsConfigure(toplevel: *XdgToplevel) bool {
+    const inflight = &toplevel.window.inflight;
+    const current = &toplevel.window.current;
+
+    // Never send configures to hidden windows.
+    // If transitioning from hidden to not-hidden, send a configure.
+    if (inflight.hidden) return false;
+    if (current.hidden) return true;
+
+    if (inflight.box.width == 0 or inflight.box.width != current.box.width or
+        inflight.box.height == 0 or inflight.box.height != current.box.height)
+    {
+        return true;
+    }
+
+    if ((inflight.focus != 0) != (current.focus != 0)) return true;
+    if (inflight.ssd != current.ssd) return true;
+    if (!std.meta.eql(inflight.tiled, current.tiled)) return true;
+    if (!std.meta.eql(inflight.capabilities, current.capabilities)) return true;
+    if (inflight.maximized != current.maximized) return true;
+    if (inflight.fullscreen != current.fullscreen) return true;
+    if (inflight.resizing != current.resizing) return true;
+
+    return false;
+}
+
 pub fn destroyPopups(toplevel: XdgToplevel) void {
     var it = toplevel.wlr_toplevel.base.popups.safeIterator(.forward);
     while (it.next()) |wlr_xdg_popup| wlr_xdg_popup.destroy();
diff --git a/river/XwaylandWindow.zig b/river/XwaylandWindow.zig
index 34dba95..b04f041 100644
--- a/river/XwaylandWindow.zig
+++ b/river/XwaylandWindow.zig
@@ -88,30 +88,44 @@ pub fn create(xsurface: *wlr.XwaylandSurface) error{OutOfMemory}!void {
 }
 
 /// Always returns false as we do not care about frame perfection for Xwayland windows.
-pub fn configure(xwindow: XwaylandWindow) bool {
+pub fn configure(xwindow: *XwaylandWindow) bool {
     const inflight = &xwindow.window.inflight;
     const current = &xwindow.window.current;
 
-    if (xwindow.xsurface.x == inflight.box.x and
-        xwindow.xsurface.y == inflight.box.y and
-        xwindow.xsurface.width == inflight.box.width and
-        xwindow.xsurface.height == inflight.box.height and
-        (inflight.focus != 0) == (current.focus != 0))
-        // TODO fullscreen
-    {
-        return false;
+    // Sending a 0 width/height to X11 clients is invalid, so fake it
+    if (inflight.box.width == 0) {
+        inflight.box.width = xwindow.window.pending.box.width;
+    }
+    if (inflight.box.height == 0) {
+        inflight.box.height = xwindow.window.pending.box.height;
     }
 
-    xwindow.xsurface.configure(
-        math.lossyCast(i16, inflight.box.x),
-        math.lossyCast(i16, inflight.box.y),
-        math.lossyCast(u16, inflight.box.width),
-        math.lossyCast(u16, inflight.box.height),
-    );
+    if (inflight.hidden != current.hidden) {
+        xwindow.xsurface.setWithdrawn(inflight.hidden);
+    }
 
-    xwindow.setActivated(inflight.focus != 0);
+    if (inflight.box.x != current.box.x or
+        inflight.box.y != current.box.y or
+        inflight.box.width != current.box.width or
+        inflight.box.height != current.box.height)
+    {
+        xwindow.xsurface.configure(
+            math.lossyCast(i16, inflight.box.x),
+            math.lossyCast(i16, inflight.box.y),
+            math.lossyCast(u16, inflight.box.width),
+            math.lossyCast(u16, inflight.box.height),
+        );
+    }
 
-    if (false) xwindow.xsurface.setFullscreen();
+    if ((inflight.focus != 0) != (current.focus != 0)) {
+        xwindow.setActivated(inflight.focus != 0);
+    }
+    if (inflight.maximized != current.maximized) {
+        xwindow.xsurface.setFullscreen(inflight.maximized);
+    }
+    if (inflight.fullscreen != current.fullscreen) {
+        xwindow.xsurface.setFullscreen(inflight.fullscreen);
+    }
 
     return false;
 }
@@ -215,7 +229,6 @@ fn handleRequestConfigure(
         return;
     }
 
-    // Allow xwayland windows to set their own dimensions (but not position) if floating
     xwindow.window.pending.box.width = event.width;
     xwindow.window.pending.box.height = event.height;
     server.wm.dirtyPending();