ext-session-lock: implement protocol

This commit is contained in:
Isaac Freund 2021-12-30 04:27:50 +00:00
parent 78a46c316a
commit 33187e0b09
No known key found for this signature in database
GPG Key ID: 86DED400DDFD7A11
11 changed files with 332 additions and 20 deletions

View File

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

View File

@ -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:

View File

@ -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 {

124
river/LockManager.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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);
}

123
river/LockSurface.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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 });
}

View File

@ -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 },

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

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

View File

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