diff --git a/build.zig b/build.zig
index 659d93d..9497301 100644
--- a/build.zig
+++ b/build.zig
@@ -88,6 +88,7 @@ pub fn build(b: *zbs.Builder) !void {
const scanner = ScanProtocolsStep.create(b);
scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
+ scanner.addSystemProtocol("staging/ext-session-lock/ext-session-lock-v1.xml");
scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml");
scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml");
scanner.addProtocolPath("protocol/river-control-unstable-v1.xml");
@@ -108,6 +109,7 @@ pub fn build(b: *zbs.Builder) !void {
scanner.generate("xdg_wm_base", 2);
scanner.generate("zwp_pointer_gestures_v1", 3);
scanner.generate("zwp_pointer_constraints_v1", 1);
+ scanner.generate("ext_session_lock_manager_v1", 1);
scanner.generate("zriver_control_v1", 1);
scanner.generate("zriver_status_manager_v1", 3);
diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd
index 5fb392e..d936308 100644
--- a/doc/riverctl.1.scd
+++ b/doc/riverctl.1.scd
@@ -154,11 +154,11 @@ are ignored by river.
## MAPPINGS
-Mappings are modal in river. Each mapping is associated with a mode and is
-only active while in that mode. There are two special modes: "normal" and
-"locked". The normal mode is the initial mode on startup. The locked mode
-is automatically entered while a lock screen is active. It cannot be entered
-or exited manually.
+Mappings are modal in river. Each mapping is associated with a mode and
+is only active while in that mode. There are two special modes: "normal"
+and "locked". The normal mode is the initial mode on startup. The locked
+mode is automatically entered while the session is locked (e.g. due to
+a screenlocker). It cannot be entered or exited manually.
The following modifiers are available for use in mappings:
diff --git a/river/Cursor.zig b/river/Cursor.zig
index e0d7f5c..b09dbff 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -32,6 +32,7 @@ const util = @import("util.zig");
const Config = @import("Config.zig");
const LayerSurface = @import("LayerSurface.zig");
+const LockSurface = @import("LockSurface.zig");
const Output = @import("Output.zig");
const Seat = @import("Seat.zig");
const View = @import("View.zig");
@@ -337,7 +338,6 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P
fn updateKeyboardFocus(self: Self, result: SurfaceAtResult) void {
switch (result.parent) {
.view => |view| {
- // Otherwise focus the view
self.seat.focus(view);
},
.layer_surface => |layer_surface| {
@@ -350,6 +350,10 @@ fn updateKeyboardFocus(self: Self, result: SurfaceAtResult) void {
self.seat.focus(null);
}
},
+ .lock_surface => |lock_surface| {
+ assert(server.lock_manager.locked);
+ self.seat.setFocusRaw(.{ .lock_surface = lock_surface });
+ },
.xwayland_override_redirect => |override_redirect| {
if (!build_options.xwayland) unreachable;
if (override_redirect.xwayland_surface.overrideRedirectWantsFocus() and
@@ -628,6 +632,7 @@ const SurfaceAtResult = struct {
parent: union(enum) {
view: *View,
layer_surface: *LayerSurface,
+ lock_surface: *LockSurface,
xwayland_override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else void,
},
};
@@ -650,6 +655,22 @@ fn surfaceAtCoords(lx: f64, ly: f64) ?SurfaceAtResult {
var oy = ly;
server.root.output_layout.outputCoords(wlr_output, &ox, &oy);
+ if (server.lock_manager.locked) {
+ if (output.lock_surface) |lock_surface| {
+ var sx: f64 = undefined;
+ var sy: f64 = undefined;
+ if (lock_surface.wlr_lock_surface.surface.surfaceAt(ox, oy, &sx, &sy)) |found| {
+ return SurfaceAtResult{
+ .surface = found,
+ .sx = sx,
+ .sy = sy,
+ .parent = .{ .lock_surface = lock_surface },
+ };
+ }
+ }
+ return null;
+ }
+
// Find the first visible fullscreen view in the stack if there is one
var it = ViewStack(View).iter(output.views.first, .forward, output.current.tags, surfaceAtFilter);
const fullscreen_view = while (it.next()) |view| {
@@ -1010,7 +1031,7 @@ pub fn checkFocusFollowsCursor(self: *Self) void {
server.root.startTransaction();
}
},
- .layer_surface => {},
+ .layer_surface, .lock_surface => {},
.xwayland_override_redirect => assert(build_options.xwayland),
}
}
@@ -1050,6 +1071,7 @@ fn shouldPassthrough(self: Self) bool {
return false;
},
.resize, .move => {
+ assert(!server.lock_manager.locked);
const target = if (self.mode == .resize) self.mode.resize.view else self.mode.move.view;
// The target view is no longer visible, is part of the layout, or is fullscreen.
return target.current.tags & target.output.current.tags == 0 or
@@ -1064,6 +1086,7 @@ fn passthrough(self: *Self, time: u32) void {
assert(self.mode == .passthrough);
if (self.surfaceAt()) |result| {
+ assert((result.parent == .lock_surface) == server.lock_manager.locked);
self.seat.wlr_seat.pointerNotifyEnter(result.surface, result.sx, result.sy);
self.seat.wlr_seat.pointerNotifyMotion(time, result.sx, result.sy);
} else {
diff --git a/river/LockManager.zig b/river/LockManager.zig
new file mode 100644
index 0000000..4918638
--- /dev/null
+++ b/river/LockManager.zig
@@ -0,0 +1,124 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2021 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 .
+
+const LockManager = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const LockSurface = @import("LockSurface.zig");
+
+locked: bool = false,
+lock: ?*wlr.SessionLockV1 = null,
+
+new_lock: wl.Listener(*wlr.SessionLockV1) = wl.Listener(*wlr.SessionLockV1).init(handleLock),
+unlock: wl.Listener(void) = wl.Listener(void).init(handleUnlock),
+destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
+new_surface: wl.Listener(*wlr.SessionLockSurfaceV1) =
+ wl.Listener(*wlr.SessionLockSurfaceV1).init(handleSurface),
+
+pub fn init(manager: *LockManager) !void {
+ manager.* = .{};
+ const wlr_manager = try wlr.SessionLockManagerV1.create(server.wl_server);
+ wlr_manager.events.new_lock.add(&manager.new_lock);
+}
+
+pub fn deinit(manager: *LockManager) void {
+ // deinit() should only be called after wl.Server.destroyClients()
+ assert(manager.lock == null);
+
+ manager.new_lock.link.remove();
+}
+
+fn handleLock(listener: *wl.Listener(*wlr.SessionLockV1), lock: *wlr.SessionLockV1) void {
+ const manager = @fieldParentPtr(LockManager, "new_lock", listener);
+
+ if (manager.lock != null) {
+ lock.destroy();
+ return;
+ }
+
+ manager.lock = lock;
+ lock.sendLocked();
+
+ if (!manager.locked) {
+ manager.locked = true;
+
+ var it = server.input_manager.seats.first;
+ while (it) |node| : (it = node.next) {
+ const seat = &node.data;
+ seat.setFocusRaw(.none);
+ seat.cursor.updateState();
+
+ // Enter locked mode
+ seat.prev_mode_id = seat.mode_id;
+ seat.enterMode(1);
+ }
+ }
+
+ lock.events.new_surface.add(&manager.new_surface);
+ lock.events.unlock.add(&manager.unlock);
+ lock.events.destroy.add(&manager.destroy);
+}
+
+fn handleUnlock(listener: *wl.Listener(void)) void {
+ const manager = @fieldParentPtr(LockManager, "unlock", listener);
+
+ assert(manager.locked);
+ manager.locked = false;
+
+ {
+ var it = server.input_manager.seats.first;
+ while (it) |node| : (it = node.next) {
+ const seat = &node.data;
+ seat.setFocusRaw(.none);
+ seat.focus(null);
+ seat.cursor.updateState();
+
+ // Exit locked mode
+ seat.enterMode(seat.prev_mode_id);
+ }
+ }
+
+ handleDestroy(&manager.destroy);
+}
+
+fn handleDestroy(listener: *wl.Listener(void)) void {
+ const manager = @fieldParentPtr(LockManager, "destroy", listener);
+
+ manager.new_surface.link.remove();
+ manager.unlock.link.remove();
+ manager.destroy.link.remove();
+
+ manager.lock = null;
+}
+
+fn handleSurface(
+ listener: *wl.Listener(*wlr.SessionLockSurfaceV1),
+ wlr_lock_surface: *wlr.SessionLockSurfaceV1,
+) void {
+ const manager = @fieldParentPtr(LockManager, "new_surface", listener);
+
+ assert(manager.locked);
+ assert(manager.lock != null);
+
+ LockSurface.create(wlr_lock_surface);
+}
diff --git a/river/LockSurface.zig b/river/LockSurface.zig
new file mode 100644
index 0000000..a9e4b81
--- /dev/null
+++ b/river/LockSurface.zig
@@ -0,0 +1,123 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2021 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 .
+
+const LockSurface = @This();
+
+const std = @import("std");
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Output = @import("Output.zig");
+const Subsurface = @import("Subsurface.zig");
+
+wlr_lock_surface: *wlr.SessionLockSurfaceV1,
+
+output_mode: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleOutputMode),
+map: wl.Listener(void) = wl.Listener(void).init(handleMap),
+destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
+commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
+new_subsurface: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleSubsurface),
+
+pub fn create(wlr_lock_surface: *wlr.SessionLockSurfaceV1) void {
+ const lock_surface = util.gpa.create(LockSurface) catch {
+ wlr_lock_surface.resource.getClient().postNoMemory();
+ return;
+ };
+
+ lock_surface.* = .{
+ .wlr_lock_surface = wlr_lock_surface,
+ };
+ wlr_lock_surface.output.events.mode.add(&lock_surface.output_mode);
+ wlr_lock_surface.events.map.add(&lock_surface.map);
+ wlr_lock_surface.events.destroy.add(&lock_surface.destroy);
+ wlr_lock_surface.surface.events.commit.add(&lock_surface.commit);
+ wlr_lock_surface.surface.events.new_subsurface.add(&lock_surface.new_subsurface);
+
+ handleOutputMode(&lock_surface.output_mode, wlr_lock_surface.output);
+
+ Subsurface.handleExisting(wlr_lock_surface.surface, .{ .lock_surface = lock_surface });
+}
+
+pub fn output(lock_surface: *LockSurface) *Output {
+ return @intToPtr(*Output, lock_surface.wlr_lock_surface.output.data);
+}
+
+fn handleOutputMode(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
+ const lock_surface = @fieldParentPtr(LockSurface, "output_mode", listener);
+
+ var output_width: i32 = undefined;
+ var output_height: i32 = undefined;
+ lock_surface.output().wlr_output.effectiveResolution(&output_width, &output_height);
+ _ = lock_surface.wlr_lock_surface.configure(@intCast(u32, output_width), @intCast(u32, output_height));
+}
+
+fn handleMap(listener: *wl.Listener(void)) void {
+ const lock_surface = @fieldParentPtr(LockSurface, "map", listener);
+
+ {
+ var it = server.input_manager.seats.first;
+ while (it) |node| : (it = node.next) {
+ const seat = &node.data;
+ if (seat.focused != .lock_surface) {
+ seat.setFocusRaw(.{ .lock_surface = lock_surface });
+ }
+ }
+ }
+
+ lock_surface.output().lock_surface = lock_surface;
+}
+
+fn handleDestroy(listener: *wl.Listener(void)) void {
+ const lock_surface = @fieldParentPtr(LockSurface, "destroy", listener);
+
+ lock_surface.output().lock_surface = null;
+ lock_surface.output().damage.addWhole();
+
+ {
+ var it = server.input_manager.seats.first;
+ while (it) |node| : (it = node.next) {
+ const seat = &node.data;
+ if (seat.focused == .lock_surface and seat.focused.lock_surface == lock_surface) {
+ seat.setFocusRaw(.none);
+ }
+ seat.cursor.updateState();
+ }
+ }
+
+ lock_surface.output_mode.link.remove();
+ lock_surface.map.link.remove();
+ lock_surface.destroy.link.remove();
+ lock_surface.commit.link.remove();
+ lock_surface.new_subsurface.link.remove();
+
+ Subsurface.destroySubsurfaces(lock_surface.wlr_lock_surface.surface);
+
+ util.gpa.destroy(lock_surface);
+}
+
+fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
+ const lock_surface = @fieldParentPtr(LockSurface, "commit", listener);
+
+ lock_surface.output().damage.addWhole();
+}
+
+fn handleSubsurface(listener: *wl.Listener(*wlr.Subsurface), subsurface: *wlr.Subsurface) void {
+ const lock_surface = @fieldParentPtr(LockSurface, "new_subsurface", listener);
+ Subsurface.create(subsurface, .{ .lock_surface = lock_surface });
+}
diff --git a/river/Output.zig b/river/Output.zig
index ca4dbff..15adc7e 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -33,6 +33,7 @@ const util = @import("util.zig");
const LayerSurface = @import("LayerSurface.zig");
const Layout = @import("Layout.zig");
const LayoutDemand = @import("LayoutDemand.zig");
+const LockSurface = @import("LockSurface.zig");
const View = @import("View.zig");
const ViewStack = @import("view_stack.zig").ViewStack;
const OutputStatus = @import("OutputStatus.zig");
@@ -67,6 +68,8 @@ usable_box: wlr.Box,
/// The top of the stack is the "most important" view.
views: ViewStack(View) = .{},
+lock_surface: ?*LockSurface = null,
+
/// The double-buffered state of the output.
current: State = State{ .tags = 1 << 0 },
pending: State = State{ .tags = 1 << 0 },
diff --git a/river/Seat.zig b/river/Seat.zig
index 482d57c..8d5c237 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -35,6 +35,7 @@ const Keyboard = @import("Keyboard.zig");
const KeyboardGroup = @import("KeyboardGroup.zig");
const KeycodeSet = @import("KeycodeSet.zig");
const LayerSurface = @import("LayerSurface.zig");
+const LockSurface = @import("LockSurface.zig");
const Mapping = @import("Mapping.zig");
const Output = @import("Output.zig");
const SeatStatus = @import("SeatStatus.zig");
@@ -50,6 +51,7 @@ const FocusTarget = union(enum) {
view: *View,
xwayland_override_redirect: *XwaylandOverrideRedirect,
layer: *LayerSurface,
+ lock_surface: *LockSurface,
none: void,
};
@@ -76,7 +78,6 @@ keyboard_groups: std.TailQueue(KeyboardGroup) = .{},
/// is currently available for focus.
focused_output: *Output,
-/// Currently focused view/layer surface if any
focused: FocusTarget = .none,
/// Stack of views in most recently focused order
@@ -149,6 +150,9 @@ pub fn deinit(self: *Self) void {
pub fn focus(self: *Self, _target: ?*View) void {
var target = _target;
+ // Views may not recieve focus while locked.
+ if (server.lock_manager.locked) return;
+
// While a layer surface is focused, views may not recieve focus
if (self.focused == .layer) return;
@@ -220,6 +224,7 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void {
break :blk target_override_redirect.xwayland_surface.surface;
},
.layer => |target_layer| target_layer.wlr_layer_surface.surface,
+ .lock_surface => |lock_surface| lock_surface.wlr_lock_surface.surface,
.none => null,
};
@@ -229,18 +234,23 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void {
view.pending.focus -= 1;
if (view.pending.focus == 0) view.setActivated(false);
},
- .xwayland_override_redirect, .layer, .none => {},
+ .xwayland_override_redirect, .layer, .lock_surface, .none => {},
}
// Set the new focus
switch (new_focus) {
.view => |target_view| {
+ assert(!server.lock_manager.locked);
assert(self.focused_output == target_view.output);
if (target_view.pending.focus == 0) target_view.setActivated(true);
target_view.pending.focus += 1;
target_view.pending.urgent = false;
},
- .layer => |target_layer| assert(self.focused_output == target_layer.output),
+ .layer => |target_layer| {
+ assert(!server.lock_manager.locked);
+ assert(self.focused_output == target_layer.output);
+ },
+ .lock_surface => assert(server.lock_manager.locked),
.xwayland_override_redirect, .none => {},
}
self.focused = new_focus;
diff --git a/river/Server.zig b/river/Server.zig
index c795013..ab1e294 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -30,6 +30,7 @@ const DecorationManager = @import("DecorationManager.zig");
const InputManager = @import("InputManager.zig");
const LayerSurface = @import("LayerSurface.zig");
const LayoutManager = @import("LayoutManager.zig");
+const LockManager = @import("LockManager.zig");
const Output = @import("Output.zig");
const Root = @import("Root.zig");
const StatusManager = @import("StatusManager.zig");
@@ -71,6 +72,7 @@ control: Control,
status_manager: StatusManager,
layout_manager: LayoutManager,
idle_inhibitor_manager: IdleInhibitorManager,
+lock_manager: LockManager,
pub fn init(self: *Self) !void {
self.wl_server = try wl.Server.create();
@@ -127,6 +129,7 @@ pub fn init(self: *Self) !void {
try self.status_manager.init();
try self.layout_manager.init();
try self.idle_inhibitor_manager.init(self.input_manager.idle);
+ try self.lock_manager.init();
// These all free themselves when the wl_server is destroyed
_ = try wlr.DataDeviceManager.create(self.wl_server);
@@ -153,6 +156,7 @@ pub fn deinit(self: *Self) void {
self.root.deinit();
self.input_manager.deinit();
self.idle_inhibitor_manager.deinit();
+ self.lock_manager.deinit();
self.wl_server.destroy();
diff --git a/river/Subsurface.zig b/river/Subsurface.zig
index 3f61c9c..3456e48 100644
--- a/river/Subsurface.zig
+++ b/river/Subsurface.zig
@@ -26,17 +26,20 @@ const util = @import("util.zig");
const DragIcon = @import("DragIcon.zig");
const LayerSurface = @import("LayerSurface.zig");
+const LockSurface = @import("LockSurface.zig");
const XdgToplevel = @import("XdgToplevel.zig");
pub const Parent = union(enum) {
xdg_toplevel: *XdgToplevel,
layer_surface: *LayerSurface,
+ lock_surface: *LockSurface,
drag_icon: *DragIcon,
pub fn damageWholeOutput(parent: Parent) void {
switch (parent) {
.xdg_toplevel => |xdg_toplevel| xdg_toplevel.view.output.damage.addWhole(),
.layer_surface => |layer_surface| layer_surface.output.damage.addWhole(),
+ .lock_surface => |lock_surface| lock_surface.output().damage.addWhole(),
.drag_icon => |_| {
var it = server.root.outputs.first;
while (it) |node| : (it = node.next) node.data.damage.addWhole();
diff --git a/river/XdgPopup.zig b/river/XdgPopup.zig
index 4b85561..3471181 100644
--- a/river/XdgPopup.zig
+++ b/river/XdgPopup.zig
@@ -76,7 +76,7 @@ pub fn create(wlr_xdg_popup: *wlr.XdgPopup, parent: Parent) void {
layer_surface.output.wlr_output.effectiveResolution(&box.width, &box.height);
wlr_xdg_popup.unconstrainFromBox(&box);
},
- .drag_icon => unreachable,
+ .drag_icon, .lock_surface => unreachable,
}
wlr_xdg_popup.base.events.destroy.add(&xdg_popup.surface_destroy);
diff --git a/river/render.zig b/river/render.zig
index 31a43b2..4581675 100644
--- a/river/render.zig
+++ b/river/render.zig
@@ -63,6 +63,35 @@ pub fn renderOutput(output: *Output) void {
server.renderer.begin(@intCast(u32, output.wlr_output.width), @intCast(u32, output.wlr_output.height));
+ if (server.lock_manager.locked) {
+ server.renderer.clear(&[_]f32{ 0, 0, 0, 1 }); // solid black
+
+ // TODO: this isn't frame-perfect if the output mode is changed. We
+ // could possibly delay rendering new frames after the mode change
+ // until the surface commits a buffer of the correct size.
+ if (output.lock_surface) |lock_surface| {
+ var rdata = SurfaceRenderData{
+ .output = output,
+ .output_x = 0,
+ .output_y = 0,
+ .when = &now,
+ };
+ lock_surface.wlr_lock_surface.surface.forEachSurface(
+ *SurfaceRenderData,
+ renderSurfaceIterator,
+ &rdata,
+ );
+ }
+
+ renderDragIcons(output, &now);
+
+ output.wlr_output.renderSoftwareCursors(null);
+ server.renderer.end();
+ output.wlr_output.commit() catch
+ log.err("output commit failed for {s}", .{output.wlr_output.name});
+ return;
+ }
+
// Find the first visible fullscreen view in the stack if there is one
var it = ViewStack(View).iter(output.views.first, .forward, output.current.tags, renderFilter);
const fullscreen_view = while (it.next()) |view| {
@@ -131,19 +160,10 @@ pub fn renderOutput(output: *Output) void {
renderDragIcons(output, &now);
- // Hardware cursors are rendered by the GPU on a separate plane, and can be
- // moved around without re-rendering what's beneath them - which is more
- // efficient. However, not all hardware supports hardware cursors. For this
- // reason, wlroots provides a software fallback, which we ask it to render
- // here. wlr_cursor handles configuring hardware vs software cursors for you,
- // and this function is a no-op when hardware cursors are in use.
output.wlr_output.renderSoftwareCursors(null);
- // Conclude rendering and swap the buffers, showing the final frame
- // on-screen.
server.renderer.end();
- // TODO: handle failure
output.wlr_output.commit() catch
log.err("output commit failed for {s}", .{output.wlr_output.name});
}