river/river/TabletTool.zig
Leon Henrik Plickat 3473d6c0be
TabletTool: use "pencil" cursor shape
This makes tablet tool cursors visually distinct from pointer cursors by
default. Client may of course continue to set custom cursors for tablet
tools if they have focus.

Also fixes a custom cursor set by a client persisting after the tablet
exits the client's surface until proximity out.
2024-03-18 20:43:59 +01:00

268 lines
8.8 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, "pencil");
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;
}
} else {
tool.wlr_cursor.setXcursor(tablet.device.seat.cursor.xcursor_manager, "pencil");
}
tool.wp_tool.notifyProximityOut();
}