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}); }