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