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
		
			
				
	
	
		
			266 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Zig
		
	
	
	
	
	
			
		
		
	
	
			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();
 | |
| }
 |