tablet-v2: implement tablet tool support

There is not any pointer emulation for tablet tool input. This means
that only clients implementing the tablet-v2 protocol will be able to
process tablet tool input.

Tablet pad support is TODO
This commit is contained in:
Isaac Freund 2024-03-11 14:47:01 +01:00
parent ac655593f3
commit 49a779b24d
No known key found for this signature in database
GPG Key ID: 86DED400DDFD7A11
10 changed files with 429 additions and 9 deletions

View File

@ -95,6 +95,7 @@ pub fn build(b: *Build) !void {
scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml");
scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml");
scanner.addSystemProtocol("unstable/xdg-decoration/xdg-decoration-unstable-v1.xml");
scanner.addSystemProtocol("unstable/tablet/tablet-unstable-v2.xml");
scanner.addSystemProtocol("staging/cursor-shape/cursor-shape-v1.xml");
scanner.addCustomProtocol("protocol/river-control-unstable-v1.xml");
@ -118,6 +119,7 @@ pub fn build(b: *Build) !void {
scanner.generate("xdg_wm_base", 2);
scanner.generate("zwp_pointer_gestures_v1", 3);
scanner.generate("zwp_pointer_constraints_v1", 1);
scanner.generate("zwp_tablet_manager_v2", 1);
scanner.generate("zxdg_decoration_manager_v1", 1);
scanner.generate("ext_session_lock_manager_v1", 1);
scanner.generate("wp_cursor_shape_manager_v1", 1);

2
deps/zig-wlroots vendored

@ -1 +1 @@
Subproject commit 4094b86f6ec705e2f3974406601ec986364e87a3
Subproject commit 67f9979e93129d1c2a882b8eab7bee1832ae041c

View File

@ -32,12 +32,15 @@ const util = @import("util.zig");
const Config = @import("Config.zig");
const DragIcon = @import("DragIcon.zig");
const InputDevice = @import("InputDevice.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 Tablet = @import("Tablet.zig");
const TabletTool = @import("TabletTool.zig");
const View = @import("View.zig");
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
@ -177,6 +180,15 @@ touch_motion: wl.Listener(*wlr.Touch.event.Motion) =
wl.Listener(*wlr.Touch.event.Motion).init(handleTouchMotion),
touch_frame: wl.Listener(void) = wl.Listener(void).init(handleTouchFrame),
tablet_tool_axis: wl.Listener(*wlr.Tablet.event.Axis) =
wl.Listener(*wlr.Tablet.event.Axis).init(handleTabletToolAxis),
tablet_tool_proximity: wl.Listener(*wlr.Tablet.event.Proximity) =
wl.Listener(*wlr.Tablet.event.Proximity).init(handleTabletToolProximity),
tablet_tool_tip: wl.Listener(*wlr.Tablet.event.Tip) =
wl.Listener(*wlr.Tablet.event.Tip).init(handleTabletToolTip),
tablet_tool_button: wl.Listener(*wlr.Tablet.event.Button) =
wl.Listener(*wlr.Tablet.event.Button).init(handleTabletToolButton),
pub fn init(self: *Self, seat: *Seat) !void {
const wlr_cursor = try wlr.Cursor.create();
errdefer wlr_cursor.destroy();
@ -204,8 +216,7 @@ pub fn init(self: *Self, seat: *Seat) !void {
// when the pointer moves. However, we can attach input devices to it, and
// it will generate aggregate events for all of them. In these events, we
// can choose how we want to process them, forwarding them to clients and
// moving the cursor around. See following post for more detail:
// https://drewdevault.com/2018/07/17/Input-handling-in-wlroots.html
// moving the cursor around.
wlr_cursor.events.axis.add(&self.axis);
wlr_cursor.events.button.add(&self.button);
wlr_cursor.events.frame.add(&self.frame);
@ -223,6 +234,11 @@ pub fn init(self: *Self, seat: *Seat) !void {
wlr_cursor.events.touch_down.add(&self.touch_down);
wlr_cursor.events.touch_motion.add(&self.touch_motion);
wlr_cursor.events.touch_frame.add(&self.touch_frame);
wlr_cursor.events.tablet_tool_axis.add(&self.tablet_tool_axis);
wlr_cursor.events.tablet_tool_proximity.add(&self.tablet_tool_proximity);
wlr_cursor.events.tablet_tool_tip.add(&self.tablet_tool_tip);
wlr_cursor.events.tablet_tool_button.add(&self.tablet_tool_button);
}
pub fn deinit(self: *Self) void {
@ -251,7 +267,7 @@ pub fn setTheme(self: *Self, theme: ?[*:0]const u8, _size: ?u32) !void {
if (build_options.xwayland) {
if (server.xwayland) |xwayland| {
try xcursor_manager.load(1);
const wlr_xcursor = xcursor_manager.getXcursor("left_ptr", 1).?;
const wlr_xcursor = xcursor_manager.getXcursor("default", 1).?;
const image = wlr_xcursor.images[0];
xwayland.setCursor(
image.buffer,
@ -280,7 +296,7 @@ pub fn setXcursor(self: *Self, name: [*:0]const u8) void {
}
fn clearFocus(self: *Self) void {
self.setXcursor("left_ptr");
self.setXcursor("default");
self.seat.wlr_seat.pointerNotifyClearFocus();
}
@ -557,6 +573,62 @@ fn handleTouchFrame(listener: *wl.Listener(void)) void {
self.seat.wlr_seat.touchNotifyFrame();
}
fn handleTabletToolAxis(
_: *wl.Listener(*wlr.Tablet.event.Axis),
event: *wlr.Tablet.event.Axis,
) void {
const device: *InputDevice = @ptrFromInt(event.device.data);
const tablet = @fieldParentPtr(Tablet, "device", device);
device.seat.handleActivity();
const tool = TabletTool.get(device.seat.wlr_seat, event.tool) catch return;
tool.axis(tablet, event);
}
fn handleTabletToolProximity(
_: *wl.Listener(*wlr.Tablet.event.Proximity),
event: *wlr.Tablet.event.Proximity,
) void {
const device: *InputDevice = @ptrFromInt(event.device.data);
const tablet = @fieldParentPtr(Tablet, "device", device);
device.seat.handleActivity();
const tool = TabletTool.get(device.seat.wlr_seat, event.tool) catch return;
tool.proximity(tablet, event);
}
fn handleTabletToolTip(
_: *wl.Listener(*wlr.Tablet.event.Tip),
event: *wlr.Tablet.event.Tip,
) void {
const device: *InputDevice = @ptrFromInt(event.device.data);
const tablet = @fieldParentPtr(Tablet, "device", device);
device.seat.handleActivity();
const tool = TabletTool.get(device.seat.wlr_seat, event.tool) catch return;
tool.tip(tablet, event);
}
fn handleTabletToolButton(
_: *wl.Listener(*wlr.Tablet.event.Button),
event: *wlr.Tablet.event.Button,
) void {
const device: *InputDevice = @ptrFromInt(event.device.data);
const tablet = @fieldParentPtr(Tablet, "device", device);
device.seat.handleActivity();
const tool = TabletTool.get(device.seat.wlr_seat, event.tool) catch return;
tool.button(tablet, event);
}
/// Handle the mapping for the passed button if any. Returns true if there
/// was a mapping and the button was handled.
fn handlePointerMapping(self: *Self, event: *wlr.Pointer.event.Button, view: *View) bool {

View File

@ -31,6 +31,7 @@ const server = &@import("main.zig").server;
const util = @import("util.zig");
const InputDevice = @import("InputDevice.zig");
const Tablet = @import("Tablet.zig");
pub const EventState = enum {
enabled,
@ -238,6 +239,11 @@ pub const MapToOutput = struct {
});
device.seat.cursor.wlr_cursor.mapInputToOutput(device.wlr_device, wlr_output);
if (device.wlr_device.type == .tablet_tool) {
const tablet = @fieldParentPtr(Tablet, "device", device);
tablet.output_mapping = wlr_output;
}
},
// These devices do not support being mapped to outputs.

View File

@ -30,6 +30,7 @@ const util = @import("util.zig");
const Seat = @import("Seat.zig");
const Keyboard = @import("Keyboard.zig");
const Switch = @import("Switch.zig");
const Tablet = @import("Tablet.zig");
const log = std.log.scoped(.input_manager);
@ -49,6 +50,7 @@ link: wl.list.Link,
pub fn init(device: *InputDevice, seat: *Seat, wlr_device: *wlr.InputDevice) !void {
const device_type: []const u8 = switch (wlr_device.type) {
.switch_device => "switch",
.tablet_tool => "tablet",
else => @tagName(wlr_device.type),
};
@ -77,6 +79,8 @@ pub fn init(device: *InputDevice, seat: *Seat, wlr_device: *wlr.InputDevice) !vo
.link = undefined,
};
wlr_device.data = @intFromPtr(device);
wlr_device.events.destroy.add(&device.destroy);
// Keyboard groups are implemented as "virtual" input devices which we don't want to expose
@ -106,6 +110,8 @@ pub fn deinit(device: *InputDevice) void {
device.seat.updateCapabilities();
}
device.wlr_device.data = 0;
device.* = undefined;
}
@ -129,11 +135,15 @@ fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice)
device.deinit();
util.gpa.destroy(device);
},
.tablet_tool => {
const tablet = @fieldParentPtr(Tablet, "device", device);
tablet.destroy();
},
.switch_device => {
const switch_device = @fieldParentPtr(Switch, "device", device);
switch_device.deinit();
util.gpa.destroy(switch_device);
},
.tablet_tool, .tablet_pad => unreachable,
.tablet_pad => unreachable,
}
}

View File

@ -50,6 +50,7 @@ virtual_keyboard_manager: *wlr.VirtualKeyboardManagerV1,
pointer_constraints: *wlr.PointerConstraintsV1,
input_method_manager: *wlr.InputMethodManagerV2,
text_input_manager: *wlr.TextInputManagerV3,
tablet_manager: *wlr.TabletManagerV2,
/// List of input device configurations. Ordered by glob generality, with
/// the most general towards the start and the most specific towards the end.
@ -84,6 +85,7 @@ pub fn init(self: *Self) !void {
.pointer_constraints = try wlr.PointerConstraintsV1.create(server.wl_server),
.input_method_manager = try wlr.InputMethodManagerV2.create(server.wl_server),
.text_input_manager = try wlr.TextInputManagerV3.create(server.wl_server),
.tablet_manager = try wlr.TabletManagerV2.create(server.wl_server),
.configs = std.ArrayList(InputConfig).init(util.gpa),
.devices = undefined,

View File

@ -41,6 +41,7 @@ const Output = @import("Output.zig");
const PointerConstraint = @import("PointerConstraint.zig");
const SeatStatus = @import("SeatStatus.zig");
const Switch = @import("Switch.zig");
const Tablet = @import("Tablet.zig");
const View = @import("View.zig");
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
@ -500,6 +501,10 @@ fn tryAddDevice(self: *Self, wlr_device: *wlr.InputDevice) !void {
self.cursor.wlr_cursor.attachInputDevice(wlr_device);
},
.tablet_tool => {
try Tablet.create(self, wlr_device);
self.cursor.wlr_cursor.attachInputDevice(wlr_device);
},
.switch_device => {
const switch_device = try util.gpa.create(Switch);
errdefer util.gpa.destroy(switch_device);
@ -508,7 +513,7 @@ fn tryAddDevice(self: *Self, wlr_device: *wlr.InputDevice) !void {
},
// TODO Support these types of input devices.
.tablet_tool, .tablet_pad => return,
.tablet_pad => {},
}
}
@ -523,8 +528,8 @@ pub fn updateCapabilities(self: *Self) void {
switch (device.wlr_device.type) {
.keyboard => capabilities.keyboard = true,
.touch => capabilities.touch = true,
.pointer, .switch_device => {},
.tablet_tool, .tablet_pad => unreachable,
.pointer, .switch_device, .tablet_tool => {},
.tablet_pad => unreachable,
}
}
}

View File

@ -347,6 +347,10 @@ fn handleRequestSetCursorShape(
_: *wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape),
event: *wlr.CursorShapeManagerV1.event.RequestSetShape,
) void {
// Ignore requests to set a tablet tool's cursor shape for now
// TODO(wlroots): https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3821
if (event.device_type == .tablet_tool) return;
const focused_client = event.seat_client.seat.pointer_state.focused_client;
// This can be sent by any client, so we check to make sure this one is

54
river/Tablet.zig Normal file
View File

@ -0,0 +1,54 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2024 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 Tablet = @This();
const std = @import("std");
const assert = std.debug.assert;
const wlr = @import("wlroots");
const server = &@import("main.zig").server;
const util = @import("util.zig");
const InputDevice = @import("InputDevice.zig");
const Seat = @import("Seat.zig");
const TabletTool = @import("TabletTool.zig");
device: InputDevice,
wp_tablet: *wlr.TabletV2Tablet,
output_mapping: ?*wlr.Output = null,
pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice) !void {
assert(wlr_device.type == .tablet_tool);
const tablet = try util.gpa.create(Tablet);
errdefer util.gpa.destroy(tablet);
const tablet_manager = server.input_manager.tablet_manager;
tablet.* = .{
.device = undefined,
.wp_tablet = try tablet_manager.createTabletV2Tablet(seat.wlr_seat, wlr_device),
};
try tablet.device.init(seat, wlr_device);
errdefer tablet.device.deinit();
}
pub fn destroy(tablet: *Tablet) void {
tablet.device.deinit();
util.gpa.destroy(tablet);
}

265
river/TabletTool.zig Normal file
View File

@ -0,0 +1,265 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2024 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 TabletTool = @This();
const std = @import("std");
const assert = std.debug.assert;
const math = std.math;
const wlr = @import("wlroots");
const wayland = @import("wayland");
const wl = wayland.server.wl;
const server = &@import("main.zig").server;
const util = @import("util.zig");
const Tablet = @import("Tablet.zig");
const log = std.log.scoped(.tablet_tool);
const Mode = union(enum) {
passthrough,
down: struct {
// Initial cursor position in layout coordinates
lx: f64,
ly: f64,
// Initial cursor position in surface-local coordinates
sx: f64,
sy: f64,
},
};
wp_tool: *wlr.TabletV2TabletTool,
wlr_cursor: *wlr.Cursor,
mode: Mode = .passthrough,
// A wlroots event may notify us of a change on one of these axes but not
// include the value of the other. We must always send both values to the
// client, which means we need to track this state.
tilt_x: f64 = 0,
tilt_y: f64 = 0,
destroy: wl.Listener(*wlr.TabletTool) = wl.Listener(*wlr.TabletTool).init(handleDestroy),
set_cursor: wl.Listener(*wlr.TabletV2TabletTool.event.SetCursor) =
wl.Listener(*wlr.TabletV2TabletTool.event.SetCursor).init(handleSetCursor),
pub fn get(wlr_seat: *wlr.Seat, wlr_tool: *wlr.TabletTool) error{OutOfMemory}!*TabletTool {
if (@as(?*TabletTool, @ptrFromInt(wlr_tool.data))) |tool| {
return tool;
} else {
return TabletTool.create(wlr_seat, wlr_tool);
}
}
fn create(wlr_seat: *wlr.Seat, wlr_tool: *wlr.TabletTool) error{OutOfMemory}!*TabletTool {
const tool = try util.gpa.create(TabletTool);
errdefer util.gpa.destroy(tool);
const wlr_cursor = try wlr.Cursor.create();
errdefer wlr_cursor.destroy();
wlr_cursor.attachOutputLayout(server.root.output_layout);
const tablet_manager = server.input_manager.tablet_manager;
tool.* = .{
.wp_tool = try tablet_manager.createTabletV2TabletTool(wlr_seat, wlr_tool),
.wlr_cursor = wlr_cursor,
};
wlr_tool.data = @intFromPtr(tool);
wlr_tool.events.destroy.add(&tool.destroy);
tool.wp_tool.events.set_cursor.add(&tool.set_cursor);
return tool;
}
fn handleDestroy(listener: *wl.Listener(*wlr.TabletTool), _: *wlr.TabletTool) void {
const tool = @fieldParentPtr(TabletTool, "destroy", listener);
tool.wp_tool.wlr_tool.data = 0;
tool.wlr_cursor.destroy();
tool.destroy.link.remove();
tool.set_cursor.link.remove();
util.gpa.destroy(tool);
}
fn handleSetCursor(
listener: *wl.Listener(*wlr.TabletV2TabletTool.event.SetCursor),
event: *wlr.TabletV2TabletTool.event.SetCursor,
) void {
const tool = @fieldParentPtr(TabletTool, "set_cursor", listener);
if (tool.wp_tool.focused_surface == null or
tool.wp_tool.focused_surface.?.resource.getClient() != event.seat_client.client)
{
log.debug("client tried to set cursor without focus", .{});
return;
}
if (event.serial != tool.wp_tool.proximity_serial) {
log.debug("focused client tried to set cursor with incorrect serial", .{});
return;
}
tool.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y);
}
pub fn axis(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Axis) void {
tool.wlr_cursor.attachInputDevice(tablet.device.wlr_device);
tool.wlr_cursor.mapInputToOutput(tablet.device.wlr_device, tablet.output_mapping);
if (event.updated_axes.x or event.updated_axes.y) {
// I don't own all these different types of tablet tools to test that this
// is correct for each, this is best effort from reading code/docs.
// The same goes for all the different axes events.
switch (tool.wp_tool.wlr_tool.type) {
.pen, .eraser, .brush, .pencil, .airbrush, .totem => {
tool.wlr_cursor.warpAbsolute(
tablet.device.wlr_device,
if (event.updated_axes.x) event.x else math.nan(f64),
if (event.updated_axes.y) event.y else math.nan(f64),
);
},
.lens, .mouse => {
tool.wlr_cursor.move(tablet.device.wlr_device, event.dx, event.dy);
},
}
switch (tool.mode) {
.passthrough => {
tool.passthrough(tablet);
},
.down => |data| {
tool.wp_tool.notifyMotion(
data.sx + (tool.wlr_cursor.x - data.lx),
data.sy + (tool.wlr_cursor.y - data.ly),
);
},
}
}
if (event.updated_axes.distance) {
tool.wp_tool.notifyDistance(event.distance);
}
if (event.updated_axes.pressure) {
tool.wp_tool.notifyPressure(event.pressure);
}
if (event.updated_axes.tilt_x or event.updated_axes.tilt_y) {
if (event.updated_axes.tilt_x) tool.tilt_x = event.tilt_x;
if (event.updated_axes.tilt_y) tool.tilt_y = event.tilt_y;
tool.wp_tool.notifyTilt(tool.tilt_x, tool.tilt_y);
}
if (event.updated_axes.rotation) {
tool.wp_tool.notifyRotation(event.rotation);
}
if (event.updated_axes.slider) {
tool.wp_tool.notifySlider(event.slider);
}
if (event.updated_axes.wheel) {
tool.wp_tool.notifyWheel(event.wheel_delta, 0);
}
}
pub fn proximity(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Proximity) void {
switch (event.state) {
.in => {
tool.wlr_cursor.attachInputDevice(tablet.device.wlr_device);
tool.wlr_cursor.mapInputToOutput(tablet.device.wlr_device, tablet.output_mapping);
tool.wlr_cursor.warpAbsolute(tablet.device.wlr_device, event.x, event.y);
tool.wlr_cursor.setXcursor(tablet.device.seat.cursor.xcursor_manager, "default");
tool.passthrough(tablet);
},
.out => {
tool.wp_tool.notifyProximityOut();
tool.wlr_cursor.unsetImage();
},
}
}
pub fn tip(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Tip) void {
switch (event.state) {
.down => {
assert(!tool.wp_tool.is_down);
tool.wp_tool.notifyDown();
if (server.root.at(tool.wlr_cursor.x, tool.wlr_cursor.y)) |result| {
if (result.surface != null) {
tool.mode = .{
.down = .{
.lx = tool.wlr_cursor.x,
.ly = tool.wlr_cursor.y,
.sx = result.sx,
.sy = result.sy,
},
};
}
}
},
.up => {
assert(tool.wp_tool.is_down);
tool.wp_tool.notifyUp();
tool.maybeExitDown(tablet);
},
}
}
pub fn button(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Button) void {
tool.wp_tool.notifyButton(event.button, event.state);
tool.maybeExitDown(tablet);
}
/// Exit down mode if the tool is up and there are no buttons pressed.
fn maybeExitDown(tool: *TabletTool, tablet: *Tablet) void {
if (tool.mode != .down or tool.wp_tool.is_down or tool.wp_tool.num_buttons > 0) {
return;
}
tool.mode = .passthrough;
tool.passthrough(tablet);
}
/// Send a motion event for the surface under the tablet tool's cursor if any.
/// Send a proximity_in event first if needed.
/// If there is no surface under the cursor or the surface under the cursor
/// does not support the tablet v2 protocol, send a proximity_out event.
fn passthrough(tool: *TabletTool, tablet: *Tablet) void {
if (server.root.at(tool.wlr_cursor.x, tool.wlr_cursor.y)) |result| {
if (result.data == .lock_surface) {
assert(server.lock_manager.state != .unlocked);
} else {
assert(server.lock_manager.state != .locked);
}
if (result.surface) |surface| {
tool.wp_tool.notifyProximityIn(tablet.wp_tablet, surface);
tool.wp_tool.notifyMotion(result.sx, result.sy);
return;
}
}
tool.wp_tool.notifyProximityOut();
}