river/river/TabletTool.zig
Isaac Freund 49a779b24d
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
2024-03-11 15:05:42 +01:00

266 lines
8.7 KiB
Zig

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