diff --git a/AUTHORS b/AUTHORS index ed1033a..6a84e17 100644 --- a/AUTHORS +++ b/AUTHORS @@ -3,3 +3,4 @@ The following developers have contributed code to river: Leon Henrik Plickat Marten Ringwelski Rishabh Das + Bonicgamer diff --git a/build.zig b/build.zig index 9d16c7b..306c0ba 100644 --- a/build.zig +++ b/build.zig @@ -42,6 +42,7 @@ pub fn build(b: *zbs.Builder) !void { scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml"); scanner.addSystemProtocol("unstable/xdg-output/xdg-output-unstable-v1.xml"); + scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml"); scanner.addProtocolPath("protocol/river-control-unstable-v1.xml"); scanner.addProtocolPath("protocol/river-options-unstable-v1.xml"); scanner.addProtocolPath("protocol/river-status-unstable-v1.xml"); diff --git a/deps/zig-wlroots b/deps/zig-wlroots index b38d3d5..1bdbb7a 160000 --- a/deps/zig-wlroots +++ b/deps/zig-wlroots @@ -1 +1 @@ -Subproject commit b38d3d5d2d9a5e4c748b8c01ed0d3861241661a6 +Subproject commit 1bdbb7a15a4038ff8bf7c9272c6a4d6eeb64ffa2 diff --git a/river/Cursor.zig b/river/Cursor.zig index 4ec84ed..d759dc7 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -60,6 +60,8 @@ wlr_cursor: *wlr.Cursor, pointer_gestures: *wlr.PointerGesturesV1, xcursor_manager: *wlr.XcursorManager, +constraint: ?*wlr.PointerConstraintV1 = null, + /// Number of distinct buttons currently pressed pressed_count: u32 = 0, @@ -382,7 +384,9 @@ fn handleMotionAbsolute( var ly: f64 = undefined; self.wlr_cursor.absoluteToLayoutCoords(event.device, event.x, event.y, &lx, &ly); - self.processMotion(event.device, event.time_msec, lx - self.wlr_cursor.x, ly - self.wlr_cursor.y); + const dx = lx - self.wlr_cursor.x; + const dy = ly - self.wlr_cursor.y; + self.processMotion(event.device, event.time_msec, dx, dy, dx, dy); } /// This event is forwarded by the cursor when a pointer emits a _relative_ @@ -395,7 +399,7 @@ fn handleMotion( self.seat.handleActivity(); - self.processMotion(event.device, event.time_msec, event.delta_x, event.delta_y); + self.processMotion(event.device, event.time_msec, event.delta_x, event.delta_y, event.unaccel_dx, event.unaccel_dy); } fn handleRequestSetCursor( @@ -561,14 +565,44 @@ fn leaveMode(self: *Self, event: *wlr.Pointer.event.Button) void { self.passthrough(event.time_msec); } -fn processMotion(self: *Self, device: *wlr.InputDevice, time: u32, delta_x: f64, delta_y: f64) void { +fn processMotion(self: *Self, device: *wlr.InputDevice, time: u32, delta_x: f64, delta_y: f64, unaccel_dx: f64, unaccel_dy: f64) void { + self.seat.input_manager.relative_pointer_manager.sendRelativeMotion( + self.seat.wlr_seat, + @as(u64, time) * 1000, + delta_x, + delta_y, + unaccel_dx, + unaccel_dy, + ); + var dx: f64 = delta_x; + var dy: f64 = delta_y; + if (self.constraint) |constraint| { + if (self.mode == .passthrough or self.mode == .down) { + if (constraint.type == .locked) return; + + var sx: f64 = undefined; + var sy: f64 = undefined; + const surface = self.surfaceAt(self.wlr_cursor.x, self.wlr_cursor.y, &sx, &sy); + + if (surface != constraint.surface) return; + + var sx_con: f64 = undefined; + var sy_con: f64 = undefined; + if (!wlr.region.confine(&constraint.region, sx, sy, sx + dx, sy + dy, &sx_con, &sy_con)) { + return; + } + + dx = sx_con - sx; + dy = sy_con - sy; + } + } switch (self.mode) { .passthrough => { - self.wlr_cursor.move(device, delta_x, delta_y); + self.wlr_cursor.move(device, dx, dy); self.passthrough(time); }, .down => |view| { - self.wlr_cursor.move(device, delta_x, delta_y); + self.wlr_cursor.move(device, dx, dy); // This takes surface-local coordinates const output_box = view.output.root.output_layout.getBox(view.output.wlr_output).?; self.seat.wlr_seat.pointerNotifyMotion( diff --git a/river/InputManager.zig b/river/InputManager.zig index d1c95b4..b65d78a 100644 --- a/river/InputManager.zig +++ b/river/InputManager.zig @@ -27,6 +27,7 @@ const util = @import("util.zig"); const Seat = @import("Seat.zig"); const Server = @import("Server.zig"); const View = @import("View.zig"); +const PointerConstraint = @import("PointerConstraint.zig"); const default_seat_name = "default"; @@ -37,6 +38,8 @@ new_input: wl.Listener(*wlr.InputDevice) = wl.Listener(*wlr.InputDevice).init(ha idle: *wlr.Idle, input_inhibit_manager: *wlr.InputInhibitManager, +pointer_constraints: *wlr.PointerConstraintsV1, +relative_pointer_manager: *wlr.RelativePointerManagerV1, virtual_pointer_manager: *wlr.VirtualPointerManagerV1, virtual_keyboard_manager: *wlr.VirtualKeyboardManagerV1, @@ -49,6 +52,8 @@ inhibit_activate: wl.Listener(*wlr.InputInhibitManager) = wl.Listener(*wlr.InputInhibitManager).init(handleInhibitActivate), inhibit_deactivate: wl.Listener(*wlr.InputInhibitManager) = wl.Listener(*wlr.InputInhibitManager).init(handleInhibitDeactivate), +new_pointer_constraint: wl.Listener(*wlr.PointerConstraintV1) = + wl.Listener(*wlr.PointerConstraintV1).init(handleNewPointerConstraint), new_virtual_pointer: wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer) = wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer).init(handleNewVirtualPointer), new_virtual_keyboard: wl.Listener(*wlr.VirtualKeyboardV1) = @@ -64,6 +69,8 @@ pub fn init(self: *Self, server: *Server) !void { // These are automatically freed when the display is destroyed .idle = try wlr.Idle.create(server.wl_server), .input_inhibit_manager = try wlr.InputInhibitManager.create(server.wl_server), + .pointer_constraints = try wlr.PointerConstraintsV1.create(server.wl_server), + .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), }; @@ -76,6 +83,7 @@ pub fn init(self: *Self, server: *Server) !void { server.backend.events.new_input.add(&self.new_input); self.input_inhibit_manager.events.activate.add(&self.inhibit_activate); self.input_inhibit_manager.events.deactivate.add(&self.inhibit_deactivate); + self.pointer_constraints.events.new_constraint.add(&self.new_pointer_constraint); 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); } @@ -171,6 +179,15 @@ fn handleNewInput(listener: *wl.Listener(*wlr.InputDevice), device: *wlr.InputDe self.defaultSeat().addDevice(device); } +fn handleNewPointerConstraint(listener: *wl.Listener(*wlr.PointerConstraintV1), constraint: *wlr.PointerConstraintV1) void { + const pointer_constraint = util.gpa.create(PointerConstraint) catch { + log.crit("out of memory", .{}); + return; + }; + + pointer_constraint.init(constraint); +} + fn handleNewVirtualPointer( listener: *wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer), event: *wlr.VirtualPointerManagerV1.event.NewPointer, diff --git a/river/PointerConstraint.zig b/river/PointerConstraint.zig new file mode 100644 index 0000000..cf666dd --- /dev/null +++ b/river/PointerConstraint.zig @@ -0,0 +1,132 @@ +// 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, either version 3 of the License, or +// (at your option) any later version. +// +// 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 Self = @This(); + +const build_options = @import("build_options"); +const std = @import("std"); +const wlr = @import("wlroots"); +const wl = @import("wayland").server.wl; +const pixman = @import("pixman"); + +const log = @import("log.zig"); +const util = @import("util.zig"); + +const Cursor = @import("Cursor.zig"); +const Seat = @import("Seat.zig"); +const View = @import("View.zig"); + +constraint: *wlr.PointerConstraintV1, +cursor: *Cursor, + +// zig fmt: off +destroy: wl.Listener(*wlr.PointerConstraintV1) = + wl.Listener(*wlr.PointerConstraintV1).init(handleDestroy), +set_region: wl.Listener(void) = + wl.Listener(void).init(handleSetRegion), +// zig fmt: on + +pub fn init(self: *Self, constraint: *wlr.PointerConstraintV1) void { + const seat = @intToPtr(*Seat, constraint.seat.data); + self.* = .{ + .constraint = constraint, + .cursor = &seat.cursor, + }; + + self.constraint.data = @ptrToInt(self); + + self.constraint.events.destroy.add(&self.destroy); + self.constraint.events.set_region.add(&self.set_region); + + if (seat.focused == .view and seat.focused.view.surface == self.constraint.surface) { + self.setAsActive(); + } +} + +pub fn setAsActive(self: *Self) void { + if (self.cursor.constraint == self.constraint) return; + + if (self.cursor.constraint) |constraint| { + constraint.sendDeactivated(); + } + + self.cursor.constraint = self.constraint; + + if (self.constraint.current.region.notEmpty() != 0) { + _ = self.constraint.region.intersect(&self.constraint.surface.input_region, &self.constraint.current.region); + } else { + _ = self.constraint.region.copy(&self.constraint.surface.input_region); + } + self.constrainToRegion(); + + self.constraint.sendActivated(); +} + +fn constrainToRegion(self: *Self) void { + if (self.cursor.constraint != self.constraint) return; + if (View.fromWlrSurface(self.constraint.surface)) |view| { + const cx = @floatToInt(c_int, self.cursor.wlr_cursor.x) - @intCast(c_int, view.current.box.x); + const cy = @floatToInt(c_int, self.cursor.wlr_cursor.y) - @intCast(c_int, view.current.box.y); + + var box: pixman.Box32 = undefined; + + if (self.constraint.region.containsPoint(cx, cy, &box) == 0) { + var nRects: c_int = undefined; + const rects = self.constraint.region.rectangles(&nRects); + + if (nRects > 0) { + const new_cx = @intToFloat(f64, view.current.box.x + rects.x1 + @divFloor(rects.x2, 2)); + const new_cy = @intToFloat(f64, view.current.box.y + rects.y1 + @divFloor(rects.y2, 2)); + + self.cursor.wlr_cursor.warpClosest(null, new_cx, new_cy); + } + } + } +} + +fn handleDestroy(listener: *wl.Listener(*wlr.PointerConstraintV1), constraint: *wlr.PointerConstraintV1) void { + const self = @fieldParentPtr(Self, "destroy", listener); + + self.destroy.link.remove(); + self.set_region.link.remove(); + + if (self.cursor.constraint == self.constraint) { + warpToHint(self.cursor); + + self.cursor.constraint = null; + } + + util.gpa.destroy(self); +} + +fn handleSetRegion(listener: *wl.Listener(void)) void { + const self = @fieldParentPtr(Self, "set_region", listener); + self.constrainToRegion(); +} + +pub fn warpToHint(cursor: *Cursor) void { + if (cursor.constraint) |constraint| { + if (constraint.current.committed.cursor_hint) { + if (View.fromWlrSurface(constraint.surface)) |view| { + const cx = constraint.current.cursor_hint.x + @intToFloat(f64, view.current.box.x); + const cy = constraint.current.cursor_hint.y + @intToFloat(f64, view.current.box.y); + + _ = cursor.wlr_cursor.warp(null, cx, cy); + } + } + } +} diff --git a/river/Seat.zig b/river/Seat.zig index 6431cd3..bfdac9c 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -37,6 +37,7 @@ const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; const log = std.log.scoped(.seat); +const PointerConstraint = @import("PointerConstraint.zig"); const FocusTarget = union(enum) { view: *View, @@ -223,7 +224,7 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void { } self.focused = new_focus; - // Send surface enter/leave events + // Send keyboard enter/leave events and handle pointer constraints if (target_surface) |wlr_surface| { if (self.wlr_seat.getKeyboard()) |keyboard| { self.wlr_seat.keyboardNotifyEnter( @@ -235,8 +236,22 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void { } else { self.wlr_seat.keyboardNotifyEnter(wlr_surface, null, 0, null); } + + if (self.input_manager.pointer_constraints.constraintForSurface(wlr_surface, self.wlr_seat)) |constraint| { + @intToPtr(*PointerConstraint, constraint.data).setAsActive(); + } else if (self.cursor.constraint) |constraint| { + PointerConstraint.warpToHint(&self.cursor); + constraint.sendDeactivated(); + self.cursor.constraint = null; + } } else { self.wlr_seat.keyboardClearFocus(); + + if (self.cursor.constraint) |constraint| { + PointerConstraint.warpToHint(&self.cursor); + constraint.sendDeactivated(); + self.cursor.constraint = null; + } } }