Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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);
+ }
+ }
}