Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
WindowManager: continue fleshing out state machine
river/Cursor.zig | 16 +++----
river/ForeignToplevelHandle.zig | 2 +-
river/LayerSurface.zig | 8 ++--
river/LockManager.zig | 2 +-
river/Output.zig | 6 +--
river/Root.zig | 2 +-
river/Seat.zig | 2 +-
river/Window.zig | 4 +-
river/WindowManager.zig | 90 +++++++++++++++++++++-----------------
river/XdgDecoration.zig | 2 +-
river/XdgToplevel.zig | 6 +--
river/XwaylandOverrideRedirect.zig | 2 +-
river/XwaylandWindow.zig | 6 +--
13 files changed, 78 insertions(+), 70 deletions(-)
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 85f49fa..26cd3a2 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -338,7 +338,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
cursor.mode = .passthrough;
cursor.passthrough(event.time_msec);
- server.wm.applyPending();
+ server.wm.dirtyPending();
} else {
_ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
}
@@ -375,10 +375,10 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
}
}
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
-/// Requires a call to WindowManager.applyPending()
+/// Requires a call to WindowManager.dirtyPending()
fn updateKeyboardFocus(cursor: Cursor, result: Root.AtResult) void {
switch (result.data) {
.window => |window| {
@@ -509,7 +509,7 @@ fn handleTouchDown(
}
}
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleTouchMotion(
@@ -796,7 +796,7 @@ fn enterMode(cursor: *Cursor, mode: Mode, window: *Window, xcursor_name: [*:0]co
cursor.seat.wlr_seat.pointerNotifyClearFocus();
cursor.setXcursor(xcursor_name);
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x: f64, delta_y: f64, unaccel_dx: f64, unaccel_dy: f64) void {
@@ -853,7 +853,7 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
// XXX move window
- server.wm.applyPending();
+ server.wm.dirtyPending();
},
.resize => |*data| {
dx += data.delta_x;
@@ -908,7 +908,7 @@ fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x:
data.y = box.height - data.initial_height;
}
- server.wm.applyPending();
+ server.wm.dirtyPending();
},
}
}
@@ -950,7 +950,7 @@ pub fn updateState(cursor: *Cursor) void {
.passthrough, .down => {},
inline .move, .resize => |data, mode| {
- // These conditions are checked in WindowManager.applyPending()
+ // These conditions are checked in WindowManager.dirtyPending()
assert(!data.window.current.fullscreen);
// Keep the cursor locked to the original offset from the edges of the window.
diff --git a/river/ForeignToplevelHandle.zig b/river/ForeignToplevelHandle.zig
index fb424d3..beaa607 100644
--- a/river/ForeignToplevelHandle.zig
+++ b/river/ForeignToplevelHandle.zig
@@ -86,7 +86,7 @@ fn handleForeignActivate(
const seat: *Seat = @ptrFromInt(event.seat.data);
seat.focus(window);
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleForeignFullscreen(
diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig
index 348a397..9fe11a9 100644
--- a/river/LayerSurface.zig
+++ b/river/LayerSurface.zig
@@ -108,7 +108,7 @@ fn handleMap(listener: *wl.Listener(void)) void {
if (consider) layer_surface else null,
);
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleUnmap(listener: *wl.Listener(void)) void {
@@ -118,7 +118,7 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
layer_surface.output.arrangeLayers();
handleKeyboardInteractiveExclusive(layer_surface.output, null);
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
@@ -138,13 +138,13 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
{
layer_surface.output.arrangeLayers();
handleKeyboardInteractiveExclusive(layer_surface.output, null);
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
}
/// Focus topmost keyboard-interactivity-exclusive layer surface above normal
/// content, or if none found, focus the surface given as `consider`.
-/// Requires a call to WindowManager.applyPending()
+/// Requires a call to WindowManager.dirtyPending()
fn handleKeyboardInteractiveExclusive(output: *Output, consider: ?*LayerSurface) void {
if (server.lock_manager.state != .unlocked) return;
diff --git a/river/LockManager.zig b/river/LockManager.zig
index 1a16c66..c40b124 100644
--- a/river/LockManager.zig
+++ b/river/LockManager.zig
@@ -218,7 +218,7 @@ fn handleUnlock(listener: *wl.Listener(void)) void {
handleDestroy(&manager.destroy);
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleDestroy(listener: *wl.Listener(void)) void {
diff --git a/river/Output.zig b/river/Output.zig
index e816404..b73cffb 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -237,7 +237,7 @@ pub fn layerSurfaceTree(output: Output, layer: zwlr.LayerShellV1.Layer) *wlr.Sce
/// Arrange all layer surfaces of this output and adjust the usable area.
/// Will arrange windows as well if the usable area changes.
-/// Requires a call to WindowManager.applyPending()
+/// Requires a call to WindowManager.dirtyPending()
pub fn arrangeLayers(output: *Output) void {
var full_box: wlr.Box = .{
.x = 0,
@@ -329,7 +329,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
server.root.handleOutputConfigChange() catch std.log.err("out of memory", .{});
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleRequestState(listener: *wl.Listener(*wlr.Output.event.RequestState), event: *wlr.Output.event.RequestState) void {
@@ -340,7 +340,7 @@ fn handleRequestState(listener: *wl.Listener(*wlr.Output.event.RequestState), ev
return;
};
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
// TODO double buffer output state changes for frame perfection and cleaner code.
diff --git a/river/Root.zig b/river/Root.zig
index 43ef70f..0f6fb10 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -359,7 +359,7 @@ fn processOutputConfig(
}
}
- if (action == .apply) server.wm.applyPending();
+ if (action == .apply) server.wm.dirtyPending();
if (success) {
config.sendSucceeded();
diff --git a/river/Seat.zig b/river/Seat.zig
index 8426990..48e8f98 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -129,7 +129,7 @@ pub fn deinit(seat: *Seat) void {
/// Set the current focus. If a visible window is passed it will be focused.
/// If null is passed, the top window in the stack of the focused output will be focused.
-/// Requires a call to WindowManager.applyPending()
+/// Requires a call to WindowManager.dirtyPending()
pub fn focus(seat: *Seat, target: ?*Window) void {
// Views may not receive focus while locked.
if (server.lock_manager.state != .unlocked) return;
diff --git a/river/Window.zig b/river/Window.zig
index 0f67d8f..5f806b5 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -484,7 +484,7 @@ pub fn map(window: *Window) !void {
window.foreign_toplevel_handle.map();
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
/// Called by the impl when the surface will no longer be displayed
@@ -501,7 +501,7 @@ pub fn unmap(window: *Window) void {
window.foreign_toplevel_handle.unmap();
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
pub fn notifyTitle(window: *const Window) void {
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 91f9e72..d8d1d21 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -36,8 +36,21 @@ object: ?*river.WindowManagerV1 = null,
windows: wl.list.Head(Window, .link),
+state: union(enum) {
+ idle,
+ /// An update event was sent to the window manager but has not yet been acked.
+ /// Value is the update serial
+ update_sent: u32,
+ /// An update event was sent to the window manager and has been acked but not yet committed.
+ update_acked,
+ /// The number of configures sent that have not yet been ack'd
+ configures_inflight: u32,
+} = .idle,
+
/// Pending state from windows to be sent to the wm in the next update sequence.
pending: struct {
+ /// Pending state has been modified since the last update event sent to the wm.
+ dirty: bool = false,
new_windows: wl.list.Head(Window, .link_new),
},
@@ -48,6 +61,7 @@ uncommitted: struct {
/// 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),
},
@@ -57,12 +71,11 @@ inflight: struct {
render_list: wl.list.Head(Window, .inflight_render_list_link),
},
+dirty_idle: ?*wl.EventSource = null,
+
/// Number of inflight configures sent to windows in the current transaction.
inflight_configures: u32 = 0,
transaction_timeout: *wl.EventSource,
-/// Set to true if applyPending() is called while a transaction is inflight.
-/// If true when a transaction completes, causes applyPending() to be called again.
-pending_state_dirty: bool = false,
pub fn init(wm: *WindowManager) !void {
const event_loop = server.wl_server.getEventLoop();
@@ -149,48 +162,43 @@ fn handleRequest(
}
}
-/// Trigger asynchronous application of pending state for all outputs and windows.
-/// Changes will not be applied to the scene graph until the layout generator
-/// generates a new layout for all outputs and all affected clients ack a
-/// configure and commit a new buffer.
-pub fn applyPending(wm: *WindowManager) void {
- {
- // Changes to the pending state may require a focus update to keep
- // state consistent. Instead of having focus(null) calls spread all
- // around the codebase and risk forgetting one, always ensure focus
- // state is synchronized here.
- var it = server.input_manager.seats.first;
- while (it) |node| : (it = node.next) node.data.focus(null);
- }
+pub fn dirtyPending(wm: *WindowManager) void {
+ wm.pending.dirty = true;
- // If there is already a transaction inflight, wait until it completes.
- if (wm.inflight_configures > 0) {
- wm.pending_state_dirty = true;
- return;
+ if (wm.dirty_idle == null) {
+ const event_loop = server.wl_server.getEventLoop();
+ wm.dirty_idle = event_loop.addIdle(*WindowManager, handleDirty, wm) catch {
+ log.err("out of memory", .{});
+ return;
+ };
}
- wm.pending_state_dirty = false;
+}
- {
- var it = server.input_manager.seats.first;
- while (it) |node| : (it = node.next) {
- const cursor = &node.data.cursor;
-
- switch (cursor.mode) {
- .passthrough, .down => {},
- inline .move, .resize => |data| {
- if (data.window.inflight.fullscreen) {
- cursor.mode = .passthrough;
- data.window.pending.resizing = false;
- data.window.inflight.resizing = false;
- }
- },
- }
+fn handleDirty(wm: *WindowManager) void {
+ switch (wm.state) {
+ .idle => {
+ assert(!wm.committed.dirty);
- cursor.inflight_mode = cursor.mode;
- }
+ if (wm.pending.dirty) {
+ wm.sendUpdate();
+ }
+ },
+ .update_sent, .update_acked, .configures_inflight => {},
}
+}
+
+fn sendUpdate(wm: *WindowManager) void {
+ assert(wm.state == .idle);
+
+ const wm_v1 = wm.object orelse return;
+
+ // XXX send all dirty pending state
+
+ wm.pending.dirty = false;
- wm.sendConfigures();
+ const serial = server.wl_server.nextSerial();
+ wm_v1.sendUpdate(serial);
+ wm.state = .{ .update_sent = serial };
}
fn sendConfigures(wm: *WindowManager) void {
@@ -282,7 +290,7 @@ fn commitTransaction(wm: *WindowManager) void {
server.idle_inhibit_manager.checkActive();
- if (wm.pending_state_dirty) {
- wm.applyPending();
+ if (wm.pending.dirty) {
+ wm.dirtyPending();
}
}
diff --git a/river/XdgDecoration.zig b/river/XdgDecoration.zig
index 840b9c2..0cd57c2 100644
--- a/river/XdgDecoration.zig
+++ b/river/XdgDecoration.zig
@@ -81,5 +81,5 @@ fn handleRequestMode(
.server_side => window.pending.decoration_hint = .prefers_ssd,
}
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 427451d..0ca4a56 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -395,7 +395,7 @@ fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
const toplevel: *XdgToplevel = @fieldParentPtr("request_fullscreen", listener);
if (toplevel.window.pending.fullscreen_requested != toplevel.wlr_toplevel.requested.fullscreen) {
toplevel.window.pending.fullscreen_requested = toplevel.wlr_toplevel.requested.fullscreen;
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
}
@@ -409,7 +409,7 @@ fn handleRequestMove(
// Moving windows with touch or tablet tool is not yet supported.
if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
- // XXX queue pointer_move_requested, applyPending()
+ // XXX queue pointer_move_requested, dirtyPending()
}
}
@@ -420,7 +420,7 @@ fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), ev
// Resizing windows with touch or tablet tool is not yet supported.
if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
- // XXX queue pointer_resize_requested, applyPending()
+ // XXX queue pointer_resize_requested, dirtyPending()
}
}
diff --git a/river/XwaylandOverrideRedirect.zig b/river/XwaylandOverrideRedirect.zig
index 4f4680e..835ef00 100644
--- a/river/XwaylandOverrideRedirect.zig
+++ b/river/XwaylandOverrideRedirect.zig
@@ -176,7 +176,7 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
}
}
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleSetGeometry(listener: *wl.Listener(void)) void {
diff --git a/river/XwaylandWindow.zig b/river/XwaylandWindow.zig
index a234534..e51f3f6 100644
--- a/river/XwaylandWindow.zig
+++ b/river/XwaylandWindow.zig
@@ -234,7 +234,7 @@ fn handleRequestConfigure(
// Allow xwayland windows to set their own dimensions (but not position) if floating
xwayland_window.window.pending.box.width = event.width;
xwayland_window.window.pending.box.height = event.height;
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleSetOverrideRedirect(listener: *wl.Listener(void)) void {
@@ -281,7 +281,7 @@ fn handleSetDecorations(listener: *wl.Listener(void)) void {
window.pending.decoration_hint = .prefers_ssd;
}
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
@@ -291,7 +291,7 @@ fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
if (window.pending.fullscreen_requested != xwayland_surface.fullscreen) {
window.pending.fullscreen_requested = xwayland_surface.fullscreen;
- server.wm.applyPending();
+ server.wm.dirtyPending();
}
}