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

commita6b26e9704152df737390344a0ffc2f66dfefb4d
parentfacd847be2
authorIsaac Freund <[email protected]>
date2025-03-20 09:05
Window: implement river_decoration_v1

 protocol/river-window-management-v1.xml |  35 ++++++--
 river/Decoration.zig                    | 148 ++++++++++++++++++++++++++++++++
 river/ShellSurface.zig                  |   2 +-
 river/Window.zig                        |  48 ++++++++++-
 rivercompat/Window.zig                  |  65 +++++++++++---
 rivercompat/WindowManager.zig           |   7 +-
 6 files changed, 283 insertions(+), 22 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 427c0d2..1792b20 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -612,9 +612,21 @@
       <arg name="edges" type="uint" enum="edges"/>
     </request>
 
-    <request name="get_decoration_surface">
-      <description summary="assign the river_decoration_v1 surface role">
-        Create a window decoration surface for the window and assign the
+    <request name="get_decoration_above">
+      <description summary="create a decoration surface above the window">
+        Create a window decoration surface above the window and assign the
+        river_decoration_v1 role to the surface.
+
+        Providing a wl_surface which already has a role or already has a buffer
+        attached or committed is a protocol error.
+      </description>
+      <arg name="id" type="new_id" interface="river_decoration_v1"/>
+      <arg name="surface" type="object" interface="wl_surface"/>
+    </request>
+
+    <request name="get_decoration_below">
+      <description summary="create a decoration surface below the window">
+        Create a window decoration surface below the window and assign the
         river_decoration_v1 role to the surface.
 
         Providing a wl_surface which already has a role or already has a buffer
@@ -846,11 +858,22 @@
 
   <interface name="river_decoration_v1" version="1">
     <description summary="a window decoration">
-      This surface is rendered above the window's content and above the window's
-      borders. The relative ordering of multiple decoration surfaces for the
-      same window is undefined by this protocol and left up to the compositor.
+      The rendering order of windows with decorations is follows:
+
+      1. Decorations created with get_decoration_below at the bottom
+      2. Window content
+      3. Borders configured with river_window_v1.set_borders
+      4. Decorations created with get_decoration_above at the top
+
+      The relative ordering of decoration surfaces above/below a window is
+      undefined by this protocol and left up to the compositor.
     </description>
 
+    <enum name="error">
+      <entry name="no_commit" value="0"
+        summary="failed to commit the surface before the window manager commit"/>
+    </enum>
+
     <request name="destroy" type="destructor">
       <description summary="destroy the decoration object">
         This request indicates that the client will no longer use the decoration
diff --git a/river/Decoration.zig b/river/Decoration.zig
new file mode 100644
index 0000000..7ad12dc
--- /dev/null
+++ b/river/Decoration.zig
@@ -0,0 +1,148 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 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 Decoration = @This();
+
+const build_options = @import("build_options");
+const std = @import("std");
+const assert = std.debug.assert;
+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");
+
+const Scene = @import("Scene.zig");
+
+const log = std.log.scoped(.wm);
+
+const role: wlr.Surface.Role = .{
+    .name = "river_decoration_v1",
+    .client_commit = clientCommit,
+    .commit = commit,
+    .unmap = null,
+    .destroy = null,
+};
+
+object: *river.DecorationV1,
+surface: *wlr.Surface,
+tree: *wlr.SceneTree,
+surfaces: Scene.SaveableSurfaces,
+/// Window.decorations_above/below
+link: wl.list.Link,
+
+rendering_requested: struct {
+    offset_x: i32 = 0,
+    offset_y: i32 = 0,
+    sync_next_commit: bool = false,
+} = .{},
+
+pub fn create(
+    client: *wl.Client,
+    version: u32,
+    id: u32,
+    surface: *wlr.Surface,
+    parent: *wlr.SceneTree,
+) !*Decoration {
+    const decoration_v1 = try river.DecorationV1.create(client, version, id);
+
+    if (!surface.setRole(&role, @ptrCast(decoration_v1), @intFromEnum(river.WindowManagerV1.Error.role))) {
+        return error.AlreadyHasRole;
+    }
+    surface.setRoleObject(@ptrCast(decoration_v1));
+
+    const decoration = try util.gpa.create(Decoration);
+    errdefer util.gpa.destroy(decoration);
+
+    const tree = try parent.createSceneTree();
+    errdefer tree.node.destroy();
+
+    const surfaces = try Scene.SaveableSurfaces.init(tree);
+    _ = try surfaces.tree.createSceneSubsurfaceTree(surface);
+
+    decoration.* = .{
+        .object = decoration_v1,
+        .surface = surface,
+        .tree = tree,
+        .surfaces = surfaces,
+        .link = undefined,
+    };
+
+    decoration_v1.setHandler(*Decoration, handleRequest, handleDestroy, decoration);
+
+    return decoration;
+}
+
+fn handleDestroy(_: *river.DecorationV1, decoration: *Decoration) void {
+    decoration.tree.node.destroy();
+
+    util.gpa.destroy(decoration);
+}
+
+fn handleRequest(
+    decoration_v1: *river.DecorationV1,
+    request: river.DecorationV1.Request,
+    decoration: *Decoration,
+) void {
+    assert(decoration.object == decoration_v1);
+    switch (request) {
+        .destroy => decoration_v1.destroy(),
+        .set_offset => |args| {
+            if (!server.wm.ensureRendering()) return;
+            decoration.rendering_requested.offset_x = args.x;
+            decoration.rendering_requested.offset_y = args.y;
+        },
+        .sync_next_commit => {
+            if (!server.wm.ensureRendering()) return;
+            decoration.rendering_requested.sync_next_commit = true;
+        },
+    }
+}
+
+fn clientCommit(wlr_surface: *wlr.Surface) callconv(.C) void {
+    if (wlr_surface.role != &role) return;
+    const resource = wlr_surface.role_resource orelse return;
+    const decoration: *Decoration = @ptrCast(@alignCast(resource.getUserData()));
+    if (decoration.rendering_requested.sync_next_commit) {
+        decoration.surfaces.save();
+    }
+}
+
+fn commit(wlr_surface: *wlr.Surface) callconv(.C) void {
+    if (wlr_surface.unmap_commit) {
+        wlr_surface.unmap();
+    } else if (wlr_surface.hasBuffer()) {
+        wlr_surface.map();
+    }
+}
+
+pub fn updateRenderingFinish(decoration: *Decoration) void {
+    const rendering_requested = &decoration.rendering_requested;
+    if (rendering_requested.sync_next_commit) {
+        rendering_requested.sync_next_commit = false;
+
+        if (!decoration.surfaces.saved.node.enabled) {
+            decoration.object.postError(.no_commit,
+                \\no wl_surface.commit after sync_next_commit and before update_rendering_finish
+            );
+        }
+    }
+
+    decoration.surfaces.dropSaved();
+
+    decoration.tree.node.setPosition(rendering_requested.offset_x, rendering_requested.offset_y);
+}
diff --git a/river/ShellSurface.zig b/river/ShellSurface.zig
index c77f45f..338141c 100644
--- a/river/ShellSurface.zig
+++ b/river/ShellSurface.zig
@@ -33,7 +33,7 @@ const WmNode = @import("WmNode.zig");
 const log = std.log.scoped(.wm);
 
 const role: wlr.Surface.Role = .{
-    .name = "river_scene_surface_v1",
+    .name = "river_shell_surface_v1",
     .client_commit = clientCommit,
     .commit = commit,
     .unmap = null,
diff --git a/river/Window.zig b/river/Window.zig
index fe34006..9ae8d0a 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -29,6 +29,7 @@ const river = @import("wayland").server.river;
 const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
+const Decoration = @import("Decoration.zig");
 const Output = @import("Output.zig");
 const Scene = @import("Scene.zig");
 const SceneNodeData = @import("SceneNodeData.zig");
@@ -89,7 +90,13 @@ node: WmNode,
 /// The implementation of this window
 impl: Impl,
 
+/// This is the root scene tree for the window.
+/// The trees in the following fields are in rendering order.
 tree: *wlr.SceneTree,
+
+decorations_below: wl.list.Head(Decoration, .link),
+decorations_below_tree: *wlr.SceneTree,
+
 surfaces: Scene.SaveableSurfaces,
 
 border: struct {
@@ -98,6 +105,10 @@ border: struct {
     top: *wlr.SceneRect,
     bottom: *wlr.SceneRect,
 },
+
+decorations_above: wl.list.Head(Decoration, .link),
+decorations_above_tree: *wlr.SceneTree,
+
 popup_tree: *wlr.SceneTree,
 
 /// Set to true once the window manager client has made its first commit
@@ -232,6 +243,8 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
         .node = undefined,
         .impl = impl,
         .tree = tree,
+        .decorations_below = undefined,
+        .decorations_below_tree = try tree.createSceneTree(),
         .surfaces = try Scene.SaveableSurfaces.init(tree),
         .border = .{
             .left = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
@@ -239,12 +252,17 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
             .top = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
             .bottom = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
         },
+        .decorations_above = undefined,
+        .decorations_above_tree = try tree.createSceneTree(),
         .popup_tree = popup_tree,
         .link = undefined,
     };
 
     window.node.init(.window);
 
+    window.decorations_below.init();
+    window.decorations_above.init();
+
     server.wm.windows.append(window);
 
     window.tree.node.setEnabled(false);
@@ -504,7 +522,29 @@ fn handleRequest(
             if (!server.wm.ensureWindowing()) return;
             windowing_requested.tiled = args.edges;
         },
-        .get_decoration_surface => {}, // XXX support decoration surfaces
+        inline .get_decoration_above, .get_decoration_below => |args, req| {
+            const above = req == .get_decoration_above;
+            const surface = wlr.Surface.fromWlSurface(args.surface);
+            const decoration = Decoration.create(
+                window_v1.getClient(),
+                window_v1.getVersion(),
+                args.id,
+                surface,
+                if (above) window.decorations_above_tree else window.decorations_below_tree,
+            ) catch |err| switch (err) {
+                error.OutOfMemory, error.ResourceCreateFailed => {
+                    window_v1.getClient().postNoMemory();
+                    log.err("out of memory", .{});
+                    return;
+                },
+                error.AlreadyHasRole => return,
+            };
+            if (above) {
+                window.decorations_above.append(decoration);
+            } else {
+                window.decorations_below.append(decoration);
+            }
+        },
         .set_capabilities => |args| {
             if (!server.wm.ensureWindowing()) return;
             windowing_requested.capabilities = args.caps;
@@ -653,7 +693,6 @@ pub fn updateRenderingStart(window: *Window) void {
             const dy = seat_op.y - seat_op.start_y;
             scheduled_box.x = data.start_x + dx;
             scheduled_box.y = data.start_y + dy;
-            log.debug("set x/y for move to {} {}", .{ scheduled_box.x, scheduled_box.y });
         },
         .resize => |data| {
             assert(data.seat.op != null);
@@ -762,6 +801,11 @@ pub fn updateRenderingFinish(window: *Window) void {
         rect.setSize(edge.box.width, edge.box.height);
         rect.setColor(&color);
     }
+
+    inline for (.{ &window.decorations_above, &window.decorations_below }) |decorations| {
+        var it = decorations.iterator(.forward);
+        while (it.next()) |decoration| decoration.updateRenderingFinish();
+    }
 }
 
 /// Returns null if the window is currently being destroyed and no longer has
diff --git a/rivercompat/Window.zig b/rivercompat/Window.zig
index c360091..2f64353 100644
--- a/rivercompat/Window.zig
+++ b/rivercompat/Window.zig
@@ -18,31 +18,57 @@ const Window = @This();
 
 const std = @import("std");
 const assert = std.debug.assert;
+const math = std.math;
 const wayland = @import("wayland");
 const wl = wayland.client.wl;
+const wp = wayland.client.wp;
 const river = wayland.client.river;
 
 const WindowManager = @import("WindowManager.zig");
 
 const gpa = std.heap.c_allocator;
 
-const State = struct {
-    new: bool = false,
-    closed: bool = false,
-};
-
 window_v1: *river.WindowV1,
 node_v1: *river.NodeV1,
-pending: State,
+windowing: struct {
+    new: bool = false,
+    closed: bool = false,
+},
+rendering: struct {
+    x: i32 = 0,
+    y: i32 = 0,
+    width: i32 = 0,
+    height: i32 = 0,
+} = .{},
 link: wl.list.Link,
 
+shadow_surface: *wl.Surface,
+shadow_decoration: *river.DecorationV1,
+shadow_viewport: *wp.Viewport,
+shadow_buffer: *wl.Buffer,
+
 pub fn create(window_v1: *river.WindowV1, wm: *WindowManager) void {
     const window = gpa.create(Window) catch @panic("OOM");
+
+    const shadow_surface = wm.compositor.createSurface() catch @panic("OOM");
+    const shadow_decoration = window_v1.getDecorationBelow(shadow_surface) catch @panic("OOM");
+    const shadow_viewport = wm.viewporter.getViewport(shadow_surface) catch @panic("OOM");
+    const shadow_buffer = wm.single_pixel.createU32RgbaBuffer(
+        0,
+        0,
+        0,
+        @intFromFloat(0.75 * math.maxInt(u32)),
+    ) catch @panic("OOM");
+
     window.* = .{
         .window_v1 = window_v1,
         .node_v1 = window_v1.getNode() catch @panic("OOM"),
-        .pending = .{ .new = true },
+        .windowing = .{ .new = true },
         .link = undefined,
+        .shadow_surface = shadow_surface,
+        .shadow_decoration = shadow_decoration,
+        .shadow_viewport = shadow_viewport,
+        .shadow_buffer = shadow_buffer,
     };
     wm.windows.append(window);
     window_v1.setListener(*Window, handleEvent, window);
@@ -51,9 +77,12 @@ pub fn create(window_v1: *river.WindowV1, wm: *WindowManager) void {
 fn handleEvent(window_v1: *river.WindowV1, event: river.WindowV1.Event, window: *Window) void {
     assert(window.window_v1 == window_v1);
     switch (event) {
-        .closed => window.pending.closed = true,
+        .closed => window.windowing.closed = true,
         .dimensions_hint => {},
-        .dimensions => {},
+        .dimensions => |args| {
+            window.rendering.width = args.width;
+            window.rendering.height = args.height;
+        },
         .app_id => {},
         .title => {},
         .parent => {},
@@ -70,7 +99,7 @@ fn handleEvent(window_v1: *river.WindowV1, event: river.WindowV1.Event, window:
 }
 
 pub fn updateWindowing(window: *Window, wm: *WindowManager) void {
-    if (window.pending.closed) {
+    if (window.windowing.closed) {
         window.window_v1.destroy();
         window.link.remove();
         {
@@ -86,7 +115,7 @@ pub fn updateWindowing(window: *Window, wm: *WindowManager) void {
         return;
     }
 
-    if (window.pending.new) {
+    if (window.windowing.new) {
         window.node_v1.placeTop();
         window.window_v1.useSsd();
 
@@ -111,5 +140,17 @@ pub fn updateWindowing(window: *Window, wm: *WindowManager) void {
         window.window_v1.proposeDimensions(400, 400);
     }
 
-    window.pending = .{};
+    window.windowing = .{};
+}
+
+pub fn updateRendering(window: *Window, wm: *WindowManager) void {
+    _ = wm;
+    {
+        window.shadow_surface.attach(window.shadow_buffer, 0, 0);
+        window.shadow_surface.damageBuffer(0, 0, math.maxInt(i32), math.maxInt(i32));
+        window.shadow_viewport.setDestination(window.rendering.width, window.rendering.height);
+        window.shadow_decoration.setOffset(20, 20);
+        window.shadow_decoration.syncNextCommit();
+        window.shadow_surface.commit();
+    }
 }
diff --git a/rivercompat/WindowManager.zig b/rivercompat/WindowManager.zig
index d863f93..b7743b1 100644
--- a/rivercompat/WindowManager.zig
+++ b/rivercompat/WindowManager.zig
@@ -109,5 +109,10 @@ fn updateWindowing(wm: *WindowManager) void {
 }
 
 fn updateRendering(wm: *WindowManager) void {
-    _ = wm;
+    {
+        var it = wm.windows.iterator(.forward);
+        while (it.next()) |window| {
+            window.updateRendering(wm);
+        }
+    }
 }