From b7b371cb4fe61c5c224fea0e99897f5dedab6780 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Sun, 5 Mar 2023 22:39:47 +0100 Subject: [PATCH] pointer-constraints: implement protocol Now with 50% less pointer warping! The new implementation requires the user to move the cursor into the constraint region before the constraint is activated in order to keep behavior more predictable. --- build.zig | 1 + river/Cursor.zig | 70 ++++++++--- river/InputManager.zig | 20 ++++ river/PointerConstraint.zig | 225 ++++++++++++++++++++++++++++++++++++ river/Root.zig | 26 ++--- river/SceneNodeData.zig | 2 +- river/Seat.zig | 34 ++++-- 7 files changed, 335 insertions(+), 43 deletions(-) create mode 100644 river/PointerConstraint.zig diff --git a/build.zig b/build.zig index e72d1ff..65c2db3 100644 --- a/build.zig +++ b/build.zig @@ -108,6 +108,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); diff --git a/river/Cursor.zig b/river/Cursor.zig index 5ac6076..e0949c5 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -35,6 +35,7 @@ const DragIcon = @import("DragIcon.zig"); const LayerSurface = @import("LayerSurface.zig"); const LockSurface = @import("LockSurface.zig"); const Output = @import("Output.zig"); +const PointerConstraint = @import("PointerConstraint.zig"); const Root = @import("Root.zig"); const Seat = @import("Seat.zig"); const View = @import("View.zig"); @@ -145,6 +146,11 @@ hide_cursor_timer: *wl.EventSource, hidden: bool = false, may_need_warp: bool = false, +/// The pointer constraint for the surface that currently has keyboard focus, if any. +/// This constraint is not necessarily active, activation only occurs once the cursor +/// has been moved inside the constraint region. +constraint: ?*PointerConstraint = null, + last_focus_follows_cursor_target: ?*View = null, /// Keeps track of the last known location of all touch points in layout coordinates. @@ -344,7 +350,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P } if (server.root.at(self.wlr_cursor.x, self.wlr_cursor.y)) |result| { - if (result.node == .view and self.handlePointerMapping(event, result.node.view)) { + if (result.data == .view and self.handlePointerMapping(event, result.data.view)) { // If a mapping is triggered don't send events to clients. return; } @@ -372,7 +378,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P /// Requires a call to Root.applyPending() fn updateKeyboardFocus(self: Self, result: Root.AtResult) void { - switch (result.node) { + switch (result.data) { .view => |view| { self.seat.focus(view); }, @@ -678,6 +684,10 @@ fn handleHideCursorTimeout(self: *Self) c_int { } pub fn startMove(cursor: *Self, view: *View) void { + if (cursor.constraint) |constraint| { + if (constraint.state == .active) constraint.deactivate(); + } + const new_mode: Mode = .{ .move = .{ .view = view, .offset_x = @floatToInt(i32, cursor.wlr_cursor.x) - view.current.box.x, @@ -687,6 +697,10 @@ pub fn startMove(cursor: *Self, view: *View) void { } pub fn startResize(cursor: *Self, view: *View, proposed_edges: ?wlr.Edges) void { + if (cursor.constraint) |constraint| { + if (constraint.state == .active) constraint.deactivate(); + } + const edges = blk: { if (proposed_edges) |edges| { if (edges.top or edges.bottom or edges.left or edges.right) { @@ -803,21 +817,40 @@ fn processMotion(self: *Self, device: *wlr.InputDevice, time: u32, delta_x: f64, var dx: f64 = delta_x; var dy: f64 = delta_y; + + if (self.constraint) |constraint| { + if (constraint.state == .active) { + switch (constraint.wlr_constraint.type) { + .locked => return, + .confined => constraint.confine(&dx, &dy), + } + } + } + switch (self.mode) { - .passthrough => { + .passthrough, .down => { self.wlr_cursor.move(device, dx, dy); - self.checkFocusFollowsCursor(); - self.passthrough(time); - self.updateDragIcons(); - }, - .down => |down| { - self.wlr_cursor.move(device, dx, dy); - self.seat.wlr_seat.pointerNotifyMotion( - time, - down.sx + (self.wlr_cursor.x - down.lx), - down.sy + (self.wlr_cursor.y - down.ly), - ); + + switch (self.mode) { + .passthrough => { + self.checkFocusFollowsCursor(); + self.passthrough(time); + }, + .down => |data| { + self.seat.wlr_seat.pointerNotifyMotion( + time, + data.sx + (self.wlr_cursor.x - data.lx), + data.sy + (self.wlr_cursor.y - data.ly), + ); + }, + else => unreachable, + } + self.updateDragIcons(); + + if (self.constraint) |constraint| { + constraint.maybeActivate(); + } }, .move => |*data| { dx += data.delta_x; @@ -904,7 +937,7 @@ pub fn checkFocusFollowsCursor(self: *Self) void { if (self.seat.drag == .pointer) return; if (server.config.focus_follows_cursor == .disabled) return; if (server.root.at(self.wlr_cursor.x, self.wlr_cursor.y)) |result| { - switch (result.node) { + switch (result.data) { .view => |view| { // Don't re-focus the last focused view when the mode is .normal if (server.config.focus_follows_cursor == .normal and @@ -941,6 +974,11 @@ pub fn updateState(self: *Self) void { if (self.may_need_warp) { self.warp(); } + + if (self.constraint) |constraint| { + constraint.updateState(); + } + if (self.shouldPassthrough()) { self.mode = .passthrough; var now: os.timespec = undefined; @@ -986,7 +1024,7 @@ fn passthrough(self: *Self, time: u32) void { assert(self.mode == .passthrough); if (server.root.at(self.wlr_cursor.x, self.wlr_cursor.y)) |result| { - if (result.node == .lock_surface) { + if (result.data == .lock_surface) { assert(server.lock_manager.state != .unlocked); } else { assert(server.lock_manager.state != .locked); diff --git a/river/InputManager.zig b/river/InputManager.zig index 2992f78..0d0eb72 100644 --- a/river/InputManager.zig +++ b/river/InputManager.zig @@ -29,6 +29,7 @@ const util = @import("util.zig"); const InputConfig = @import("InputConfig.zig"); const InputDevice = @import("InputDevice.zig"); const Keyboard = @import("Keyboard.zig"); +const PointerConstraint = @import("PointerConstraint.zig"); const Seat = @import("Seat.zig"); const Switch = @import("Switch.zig"); @@ -42,6 +43,7 @@ idle_notifier: *wlr.IdleNotifierV1, relative_pointer_manager: *wlr.RelativePointerManagerV1, virtual_pointer_manager: *wlr.VirtualPointerManagerV1, virtual_keyboard_manager: *wlr.VirtualKeyboardManagerV1, +pointer_constraints: *wlr.PointerConstraintsV1, configs: std.ArrayList(InputConfig), devices: wl.list.Head(InputDevice, .link), @@ -53,6 +55,8 @@ new_virtual_pointer: wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer) wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer).init(handleNewVirtualPointer), new_virtual_keyboard: wl.Listener(*wlr.VirtualKeyboardV1) = wl.Listener(*wlr.VirtualKeyboardV1).init(handleNewVirtualKeyboard), +new_constraint: wl.Listener(*wlr.PointerConstraintV1) = + wl.Listener(*wlr.PointerConstraintV1).init(handleNewConstraint), pub fn init(self: *Self) !void { const seat_node = try util.gpa.create(std.TailQueue(Seat).Node); @@ -64,6 +68,7 @@ pub fn init(self: *Self) !void { .relative_pointer_manager = try wlr.RelativePointerManagerV1.create(server.wl_server), .virtual_pointer_manager = try wlr.VirtualPointerManagerV1.create(server.wl_server), .virtual_keyboard_manager = try wlr.VirtualKeyboardManagerV1.create(server.wl_server), + .pointer_constraints = try wlr.PointerConstraintsV1.create(server.wl_server), .configs = std.ArrayList(InputConfig).init(util.gpa), .devices = undefined, @@ -78,12 +83,17 @@ pub fn init(self: *Self) !void { server.backend.events.new_input.add(&self.new_input); self.virtual_pointer_manager.events.new_virtual_pointer.add(&self.new_virtual_pointer); self.virtual_keyboard_manager.events.new_virtual_keyboard.add(&self.new_virtual_keyboard); + self.pointer_constraints.events.new_constraint.add(&self.new_constraint); } pub fn deinit(self: *Self) void { // This function must be called after the backend has been destroyed assert(self.devices.empty()); + self.new_virtual_pointer.link.remove(); + self.new_virtual_keyboard.link.remove(); + self.new_constraint.link.remove(); + while (self.seats.pop()) |seat_node| { seat_node.data.deinit(); util.gpa.destroy(seat_node); @@ -138,3 +148,13 @@ fn handleNewVirtualKeyboard( const seat = @intToPtr(*Seat, virtual_keyboard.seat.data); seat.addDevice(&virtual_keyboard.keyboard.base); } + +fn handleNewConstraint( + _: *wl.Listener(*wlr.PointerConstraintV1), + wlr_constraint: *wlr.PointerConstraintV1, +) void { + PointerConstraint.create(wlr_constraint) catch { + log.err("out of memory", .{}); + wlr_constraint.resource.postNoMemory(); + }; +} diff --git a/river/PointerConstraint.zig b/river/PointerConstraint.zig new file mode 100644 index 0000000..59fd4cf --- /dev/null +++ b/river/PointerConstraint.zig @@ -0,0 +1,225 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2023 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 PointerConstraint = @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 Seat = @import("Seat.zig"); + +const log = std.log.scoped(.pointer_constraint); + +wlr_constraint: *wlr.PointerConstraintV1, + +state: union(enum) { + inactive, + active: struct { + /// Node of the active constraint surface in the scene graph. + node: *wlr.SceneNode, + /// Coordinates of the pointer on activation in the surface coordinate system. + sx: f64, + sy: f64, + }, +} = .inactive, + +destroy: wl.Listener(*wlr.PointerConstraintV1) = wl.Listener(*wlr.PointerConstraintV1).init(handleDestroy), +set_region: wl.Listener(void) = wl.Listener(void).init(handleSetRegion), + +node_destroy: wl.Listener(void) = wl.Listener(void).init(handleNodeDestroy), + +pub fn create(wlr_constraint: *wlr.PointerConstraintV1) error{OutOfMemory}!void { + const seat = @intToPtr(*Seat, wlr_constraint.seat.data); + + const constraint = try util.gpa.create(PointerConstraint); + errdefer util.gpa.destroy(constraint); + + constraint.* = .{ + .wlr_constraint = wlr_constraint, + }; + wlr_constraint.data = @ptrToInt(constraint); + + wlr_constraint.events.destroy.add(&constraint.destroy); + wlr_constraint.events.set_region.add(&constraint.set_region); + + if (seat.wlr_seat.keyboard_state.focused_surface) |surface| { + if (surface == wlr_constraint.surface) { + assert(seat.cursor.constraint == null); + seat.cursor.constraint = constraint; + constraint.maybeActivate(); + } + } +} + +pub fn maybeActivate(constraint: *PointerConstraint) void { + const seat = @intToPtr(*Seat, constraint.wlr_constraint.seat.data); + + assert(seat.cursor.constraint == constraint); + assert(seat.wlr_seat.keyboard_state.focused_surface == constraint.wlr_constraint.surface); + + if (constraint.state == .active) return; + + if (seat.cursor.mode == .move or seat.cursor.mode == .resize) return; + + const result = server.root.at(seat.cursor.wlr_cursor.x, seat.cursor.wlr_cursor.y) orelse return; + if (result.surface != constraint.wlr_constraint.surface) return; + + const sx = @floatToInt(i32, result.sx); + const sy = @floatToInt(i32, result.sy); + if (!constraint.wlr_constraint.region.containsPoint(sx, sy, null)) return; + + assert(constraint.state == .inactive); + constraint.state = .{ + .active = .{ + .node = result.node, + .sx = result.sx, + .sy = result.sy, + }, + }; + result.node.events.destroy.add(&constraint.node_destroy); + + log.info("activating pointer constraint", .{}); + + constraint.wlr_constraint.sendActivated(); +} + +/// Called when the cursor position or content in the scene graph changes +pub fn updateState(constraint: *PointerConstraint) void { + const seat = @intToPtr(*Seat, constraint.wlr_constraint.seat.data); + + assert(seat.wlr_seat.keyboard_state.focused_surface == constraint.wlr_constraint.surface); + + constraint.maybeActivate(); + + if (constraint.state != .active) return; + + var lx: i32 = undefined; + var ly: i32 = undefined; + if (!constraint.state.active.node.coords(&lx, &ly)) { + log.info("deactivating pointer constraint, scene node disabled", .{}); + constraint.deactivate(); + return; + } + + const warp_lx = @intToFloat(f64, lx) + constraint.state.active.sx; + const warp_ly = @intToFloat(f64, ly) + constraint.state.active.sy; + if (!seat.cursor.wlr_cursor.warp(null, warp_lx, warp_ly)) { + log.info("deactivating pointer constraint, could not warp cursor", .{}); + constraint.deactivate(); + return; + } +} + +pub fn confine(constraint: *PointerConstraint, dx: *f64, dy: *f64) void { + assert(constraint.state == .active); + assert(constraint.wlr_constraint.type == .confined); + + const region = &constraint.wlr_constraint.region; + const sx = constraint.state.active.sx; + const sy = constraint.state.active.sy; + var new_sx: f64 = undefined; + var new_sy: f64 = undefined; + assert(wlr.region.confine(region, sx, sy, sx + dx.*, sy + dy.*, &new_sx, &new_sy)); + + dx.* = new_sx - sx; + dy.* = new_sy - sy; + + constraint.state.active.sx = new_sx; + constraint.state.active.sy = new_sy; +} + +pub fn deactivate(constraint: *PointerConstraint) void { + const seat = @intToPtr(*Seat, constraint.wlr_constraint.seat.data); + + assert(seat.cursor.constraint == constraint); + assert(constraint.state == .active); + + constraint.warpToHintIfSet(); + + constraint.state = .inactive; + constraint.node_destroy.link.remove(); + constraint.wlr_constraint.sendDeactivated(); +} + +fn warpToHintIfSet(constraint: *PointerConstraint) void { + const seat = @intToPtr(*Seat, constraint.wlr_constraint.seat.data); + + if (constraint.wlr_constraint.current.committed.cursor_hint) { + var lx: i32 = undefined; + var ly: i32 = undefined; + _ = constraint.state.active.node.coords(&lx, &ly); + + const sx = constraint.wlr_constraint.current.cursor_hint.x; + const sy = constraint.wlr_constraint.current.cursor_hint.y; + _ = seat.cursor.wlr_cursor.warp(null, @intToFloat(f64, lx) + sx, @intToFloat(f64, ly) + sy); + _ = seat.wlr_seat.pointerWarp(sx, sy); + } +} + +fn handleNodeDestroy(listener: *wl.Listener(void)) void { + const constraint = @fieldParentPtr(PointerConstraint, "node_destroy", listener); + + log.info("deactivating pointer constraint, scene node destroyed", .{}); + constraint.deactivate(); +} + +fn handleDestroy(listener: *wl.Listener(*wlr.PointerConstraintV1), _: *wlr.PointerConstraintV1) void { + const constraint = @fieldParentPtr(PointerConstraint, "destroy", listener); + const seat = @intToPtr(*Seat, constraint.wlr_constraint.seat.data); + + if (constraint.state == .active) { + // We can't simply call deactivate() here as it calls sendDeactivated(), + // which could in the case of a oneshot constraint lifetime recursively + // destroy the constraint. + constraint.warpToHintIfSet(); + constraint.node_destroy.link.remove(); + } + + constraint.destroy.link.remove(); + constraint.set_region.link.remove(); + + if (seat.cursor.constraint == constraint) { + seat.cursor.constraint = null; + } + + util.gpa.destroy(constraint); +} + +fn handleSetRegion(listener: *wl.Listener(void)) void { + const constraint = @fieldParentPtr(PointerConstraint, "set_region", listener); + const seat = @intToPtr(*Seat, constraint.wlr_constraint.seat.data); + + switch (constraint.state) { + .active => |state| { + const sx = @floatToInt(i32, state.sx); + const sy = @floatToInt(i32, state.sy); + if (!constraint.wlr_constraint.region.containsPoint(sx, sy, null)) { + log.info("deactivating pointer constraint, region change left pointer outside constraint", .{}); + constraint.deactivate(); + } + }, + .inactive => { + if (seat.cursor.constraint == constraint) { + constraint.maybeActivate(); + } + }, + } +} diff --git a/river/Root.zig b/river/Root.zig index a528345..752a510 100644 --- a/river/Root.zig +++ b/river/Root.zig @@ -170,15 +170,11 @@ pub fn deinit(self: *Self) void { } pub const AtResult = struct { + node: *wlr.SceneNode, surface: ?*wlr.Surface, sx: f64, sy: f64, - node: union(enum) { - view: *View, - layer_surface: *LayerSurface, - lock_surface: *LockSurface, - xwayland_override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn, - }, + data: SceneNodeData.Data, }; /// Return information about what is currently rendered in the interactive_content @@ -186,11 +182,11 @@ pub const AtResult = struct { pub fn at(self: Self, lx: f64, ly: f64) ?AtResult { var sx: f64 = undefined; var sy: f64 = undefined; - const node_at = self.interactive_content.node.at(lx, ly, &sx, &sy) orelse return null; + const node = self.interactive_content.node.at(lx, ly, &sx, &sy) orelse return null; const surface: ?*wlr.Surface = blk: { - if (node_at.type == .buffer) { - const scene_buffer = wlr.SceneBuffer.fromNode(node_at); + if (node.type == .buffer) { + const scene_buffer = wlr.SceneBuffer.fromNode(node); if (wlr.SceneSurface.fromBuffer(scene_buffer)) |scene_surface| { break :blk scene_surface.surface; } @@ -198,19 +194,13 @@ pub fn at(self: Self, lx: f64, ly: f64) ?AtResult { break :blk null; }; - if (SceneNodeData.fromNode(node_at)) |scene_node_data| { + if (SceneNodeData.fromNode(node)) |scene_node_data| { return .{ + .node = node, .surface = surface, .sx = sx, .sy = sy, - .node = switch (scene_node_data.data) { - .view => |view| .{ .view = view }, - .layer_surface => |layer_surface| .{ .layer_surface = layer_surface }, - .lock_surface => |lock_surface| .{ .lock_surface = lock_surface }, - .xwayland_override_redirect => |xwayland_override_redirect| .{ - .xwayland_override_redirect = xwayland_override_redirect, - }, - }, + .data = scene_node_data.data, }; } else { return null; diff --git a/river/SceneNodeData.zig b/river/SceneNodeData.zig index 782351f..4fda92b 100644 --- a/river/SceneNodeData.zig +++ b/river/SceneNodeData.zig @@ -27,7 +27,7 @@ const LockSurface = @import("LockSurface.zig"); const View = @import("View.zig"); const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig"); -const Data = union(enum) { +pub const Data = union(enum) { view: *View, lock_surface: *LockSurface, layer_surface: *LayerSurface, diff --git a/river/Seat.zig b/river/Seat.zig index d00ea12..685d4cc 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -38,6 +38,7 @@ const LayerSurface = @import("LayerSurface.zig"); const LockSurface = @import("LockSurface.zig"); const Mapping = @import("Mapping.zig"); const Output = @import("Output.zig"); +const PointerConstraint = @import("PointerConstraint.zig"); const SeatStatus = @import("SeatStatus.zig"); const Switch = @import("Switch.zig"); const View = @import("View.zig"); @@ -245,8 +246,33 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void { } self.focused = new_focus; + if (self.cursor.constraint) |constraint| { + if (constraint.wlr_constraint.surface != target_surface) { + if (constraint.state == .active) { + log.info("deactivating pointer constraint for surface, keyboard focus lost", .{}); + constraint.deactivate(); + } + self.cursor.constraint = null; + } + } + self.keyboardEnterOrLeave(target_surface); + if (target_surface) |surface| { + const pointer_constraints = server.input_manager.pointer_constraints; + if (pointer_constraints.constraintForSurface(surface, self.wlr_seat)) |wlr_constraint| { + if (self.cursor.constraint) |constraint| { + assert(constraint.wlr_constraint == wlr_constraint); + } else { + self.cursor.constraint = @intToPtr(*PointerConstraint, wlr_constraint.data); + } + } + } + + // Depending on configuration and cursor position, changing keyboard focus + // may cause the cursor to be warped. + self.cursor.may_need_warp = true; + // Inform any clients tracking status of the change var it = self.status_trackers.first; while (it) |node| : (it = node.next) node.data.sendFocusedView(); @@ -258,16 +284,8 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void { pub fn keyboardEnterOrLeave(self: *Self, target_surface: ?*wlr.Surface) void { if (target_surface) |wlr_surface| { self.keyboardNotifyEnter(wlr_surface); - - // Depending on configuration and cursor position, changing keyboard focus - // may cause the cursor to be warped. - self.cursor.may_need_warp = true; } else { self.wlr_seat.keyboardNotifyClearFocus(); - - // Depending on configuration and cursor position, changing keyboard focus - // may cause the cursor to be warped. - self.cursor.may_need_warp = true; } }