From 49a779b24d1a3a9999b33f52468f760153cc5d7f Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Mon, 11 Mar 2024 14:47:01 +0100 Subject: [PATCH] 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 --- build.zig | 2 + deps/zig-wlroots | 2 +- river/Cursor.zig | 80 ++++++++++++- river/InputConfig.zig | 6 + river/InputDevice.zig | 12 +- river/InputManager.zig | 2 + river/Seat.zig | 11 +- river/Server.zig | 4 + river/Tablet.zig | 54 +++++++++ river/TabletTool.zig | 265 +++++++++++++++++++++++++++++++++++++++++ 10 files changed, 429 insertions(+), 9 deletions(-) create mode 100644 river/Tablet.zig create mode 100644 river/TabletTool.zig diff --git a/build.zig b/build.zig index 5a04f5c..3361d06 100644 --- a/build.zig +++ b/build.zig @@ -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); diff --git a/deps/zig-wlroots b/deps/zig-wlroots index 4094b86..67f9979 160000 --- a/deps/zig-wlroots +++ b/deps/zig-wlroots @@ -1 +1 @@ -Subproject commit 4094b86f6ec705e2f3974406601ec986364e87a3 +Subproject commit 67f9979e93129d1c2a882b8eab7bee1832ae041c diff --git a/river/Cursor.zig b/river/Cursor.zig index a72fe64..f152c79 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -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 { diff --git a/river/InputConfig.zig b/river/InputConfig.zig index 05b12d9..46d744b 100644 --- a/river/InputConfig.zig +++ b/river/InputConfig.zig @@ -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. diff --git a/river/InputDevice.zig b/river/InputDevice.zig index 7443d5f..9e89e74 100644 --- a/river/InputDevice.zig +++ b/river/InputDevice.zig @@ -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, } } diff --git a/river/InputManager.zig b/river/InputManager.zig index 4065962..493f7b1 100644 --- a/river/InputManager.zig +++ b/river/InputManager.zig @@ -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, diff --git a/river/Seat.zig b/river/Seat.zig index 78cda16..4da4861 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -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, } } } diff --git a/river/Server.zig b/river/Server.zig index b3c47f1..91c6d78 100644 --- a/river/Server.zig +++ b/river/Server.zig @@ -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 diff --git a/river/Tablet.zig b/river/Tablet.zig new file mode 100644 index 0000000..d5773de --- /dev/null +++ b/river/Tablet.zig @@ -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 . + +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); +} diff --git a/river/TabletTool.zig b/river/TabletTool.zig new file mode 100644 index 0000000..b0f4ff4 --- /dev/null +++ b/river/TabletTool.zig @@ -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 . + +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(); +}