input: Add pointer-emulation grab for tablet tools

This commit is contained in:
Victoria Brekenfeld 2026-08-18 19:12:06 +02:00 • committed by Victoria Brekenfeld
parent 6467f79025
commit 9125f8a2a7
3 changed files with 351 additions and 23 deletions

View file

@ -9,7 +9,10 @@ use crate::{
cosmic_modifiers_from_smithay, cosmic_modifiers_from_smithay,
}, },
}, },
input::gestures::{GestureState, SwipeAction}, input::{
gestures::{GestureState, SwipeAction},
tablet_emu::PointerEmulationGrab,
},
shell::{ shell::{
SeatExt, Trigger, SeatExt, Trigger,
focus::{ focus::{
@ -52,7 +55,7 @@ use smithay::{
keyboard::KeyboardHandle, keyboard::KeyboardHandle,
keyboard::{FilterResult, KeyboardSource, KeysymHandle, ModifiersState}, keyboard::{FilterResult, KeyboardSource, KeysymHandle, ModifiersState},
pointer::{ pointer::{
AxisFrame, ButtonEvent as PointerButtonEvent, GestureHoldBeginEvent, AxisFrame, ButtonEvent as PointerButtonEvent, Focus, GestureHoldBeginEvent,
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
GestureSwipeUpdateEvent, MotionEvent as PointerMotionEvent, PointerGrab, PointerHandle, GestureSwipeUpdateEvent, MotionEvent as PointerMotionEvent, PointerGrab, PointerHandle,
@ -89,6 +92,7 @@ use std::{
pub mod actions; pub mod actions;
pub mod gestures; pub mod gestures;
pub mod tablet_emu;
/// Identifies the input backend instance an event came from, used to disambiguate device ids /// Identifies the input backend instance an event came from, used to disambiguate device ids
/// (which are only unique within a single backend instance, see /// (which are only unique within a single backend instance, see
@ -1609,6 +1613,26 @@ impl State {
let tool = tablet_seat.get_tool(&event.tool()); let tool = tablet_seat.get_tool(&event.tool());
if let Some(tool) = tool { if let Some(tool) = tool {
let serial = SERIAL_COUNTER.next_serial();
if !tool.is_grabbed()
&& under
.as_ref()
.is_some_and(|(target, _)| !target.supports_tool(&tool))
{
let start_data = tool::GrabStartData {
focus: under.clone(),
trigger: tool::GrabTrigger::Proximity,
location: position.as_logical(),
};
tool.set_grab(
self,
PointerEmulationGrab::new(start_data, seat.clone()),
event.time(),
serial,
Focus::Keep,
);
}
let frame = tool::AxisFrame { let frame = tool::AxisFrame {
pressure: event.pressure_has_changed().then(|| event.pressure()), pressure: event.pressure_has_changed().then(|| event.pressure()),
distance: event.distance_has_changed().then(|| event.distance()), distance: event.distance_has_changed().then(|| event.distance()),
@ -1629,7 +1653,7 @@ impl State {
under, under,
&tool::MotionEvent { &tool::MotionEvent {
location: position.as_logical(), location: position.as_logical(),
serial: SERIAL_COUNTER.next_serial(), serial,
time: event.time(), time: event.time(),
}, },
); );
@ -1683,6 +1707,25 @@ impl State {
if let Some(tablet) = tablet { if let Some(tablet) = tablet {
let serial = SERIAL_COUNTER.next_serial(); let serial = SERIAL_COUNTER.next_serial();
if !tool.is_grabbed()
&& under
.as_ref()
.is_some_and(|(target, _)| !target.supports_tool(&tool))
{
let start_data = tool::GrabStartData {
focus: under.clone(),
trigger: tool::GrabTrigger::Proximity,
location: position.as_logical(),
};
tool.set_grab(
self,
PointerEmulationGrab::new(start_data, seat.clone()),
event.time(),
serial,
Focus::Keep,
);
}
let frame = tool::AxisFrame { let frame = tool::AxisFrame {
pressure: event.pressure_has_changed().then(|| event.pressure()), pressure: event.pressure_has_changed().then(|| event.pressure()),
distance: event.distance_has_changed().then(|| event.distance()), distance: event.distance_has_changed().then(|| event.distance()),
@ -1697,24 +1740,39 @@ impl State {
}; };
match event.state() { match event.state() {
ProximityState::In => tool.proximity_in( ProximityState::In => {
self, tool.proximity_in(
under, self,
tablet, under,
&tool::ProximityInEvent { tablet,
location: position.as_logical(), &tool::ProximityInEvent {
axis: Some(frame), location: position.as_logical(),
serial: SERIAL_COUNTER.next_serial(), axis: Some(frame),
time: event.time(), serial,
}, time: event.time(),
), },
ProximityState::Out => tool.proximity_out( );
self, }
&tool::ProximityOutEvent { ProximityState::Out => {
serial, tool.proximity_out(
time: event.time(), self,
}, &tool::ProximityOutEvent {
), serial,
time: event.time(),
},
);
if let Some(pointer) = seat.get_pointer() {
pointer.motion(
self,
None,
&PointerMotionEvent {
location: position.as_logical(),
serial,
time: event.time(),
},
);
}
}
} }
tool.frame(self, event.time()); tool.frame(self, event.time());

260
src/input/tablet_emu.rs Normal file
View file

@ -0,0 +1,260 @@
// SPDX-License-Identifier: GPL-3.0-only
//! Pointer Emulating TabletTool grab.
use crate::{shell::focus::target::PointerFocusTarget, state::State};
use smithay::{
backend::input::{ButtonState, InputTime, MouseButton},
input::{
Seat, pointer,
tablet::{
self,
tool::{
AxisFrame, ButtonEvent, DownEvent, GrabStartData, ProximityInEvent,
ProximityOutEvent, TabletToolGrab, TabletToolInnerHandle, UpEvent,
},
},
},
utils::{Logical, Point, SERIAL_COUNTER},
};
// from https://gitlab.freedesktop.org/libinput/libinput/-/blob/main/include/linux/linux/input-event-codes.h
const BTN_STYLUS: u32 = 0x14b;
const BTN_STYLUS_2: u32 = 0x14c;
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
const BTN_MIDDLE: u32 = 0x112;
const BTN_FORWARD: u32 = 0x115;
const BTN_BACK: u32 = 0x116;
pub struct PointerEmulationGrab {
start_data: GrabStartData<State>,
seat: Seat<State>,
above_source: bool,
tip_down: bool,
button_down: Vec<u32>,
}
impl PointerEmulationGrab {
pub fn new(start_data: GrabStartData<State>, seat: Seat<State>) -> Self {
Self {
start_data,
seat,
above_source: true,
tip_down: false,
button_down: Vec::new(),
}
}
fn button_to_mouse(&self, button: u32) -> Option<MouseButton> {
// FIXME: This should be configurable.
if button == BTN_STYLUS {
Some(MouseButton::Right)
} else if button == BTN_STYLUS_2 {
Some(MouseButton::Middle)
} else {
None
}
}
fn pointer_button(&self, data: &mut State, button: MouseButton, state: ButtonState) {
if let Some(pointer) = self.seat.get_pointer() {
let button = match button {
MouseButton::Left => BTN_LEFT,
MouseButton::Right => BTN_RIGHT,
MouseButton::Middle => BTN_MIDDLE,
MouseButton::Back => BTN_BACK,
MouseButton::Forward => BTN_FORWARD,
_ => return,
};
pointer.button(
data,
&pointer::ButtonEvent {
serial: SERIAL_COUNTER.next_serial(),
button,
state,
time: InputTime::now(),
},
);
pointer.frame(data);
}
}
}
type Type = tablet::tool::MotionEvent;
impl TabletToolGrab<State> for PointerEmulationGrab {
fn proximity_in(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
event: &ProximityInEvent,
) {
handle.proximity_in(data, focus.clone(), event);
if !self.above_source && !self.tip_down && self.button_down.is_empty() {
handle.unset_grab(
self,
data,
SERIAL_COUNTER.next_serial(),
InputTime::now(),
true,
);
} else {
if let Some(pointer) = self.seat.get_pointer() {
pointer.motion(
data,
focus,
&pointer::MotionEvent {
location: event.location,
serial: SERIAL_COUNTER.next_serial(),
time: InputTime::now(),
},
);
pointer.frame(data);
}
}
}
fn proximity_out(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
event: &ProximityOutEvent,
) {
handle.proximity_out(data, event);
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn down(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
event: &DownEvent,
) {
handle.down(data, event);
self.tip_down = true;
self.pointer_button(data, MouseButton::Left, ButtonState::Pressed);
}
fn up(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
event: &UpEvent,
) {
handle.up(data, event);
self.pointer_button(data, MouseButton::Left, ButtonState::Released);
self.tip_down = false;
if !self.above_source && !self.tip_down && self.button_down.is_empty() {
handle.unset_grab(self, data, event.serial, event.time, true);
}
}
fn motion(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
event: &Type,
) {
self.above_source = focus.as_ref().map(|(target, _)| target)
== self.start_data.focus.as_ref().map(|(target, _)| target);
handle.motion(data, self.start_data.focus.clone(), event);
if !self.above_source && !self.tip_down && self.button_down.is_empty() {
handle.unset_grab(
self,
data,
SERIAL_COUNTER.next_serial(),
InputTime::now(),
true,
);
} else {
if let Some(pointer) = self.seat.get_pointer() {
pointer.motion(
data,
focus,
&pointer::MotionEvent {
location: event.location,
serial: SERIAL_COUNTER.next_serial(),
time: InputTime::now(),
},
);
pointer.frame(data);
}
}
}
fn button(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
self.button_down.retain(|b| b != &event.button);
if matches!(event.state, ButtonState::Pressed) {
self.button_down.push(event.button);
}
if let Some(button) = self.button_to_mouse(event.button) {
self.pointer_button(data, button, event.state);
}
if !self.above_source && !self.tip_down && self.button_down.is_empty() {
handle.unset_grab(
self,
data,
SERIAL_COUNTER.next_serial(),
InputTime::now(),
true,
);
}
}
fn axis(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
frame: AxisFrame,
) {
handle.axis(data, frame);
}
fn frame(
&mut self,
data: &mut State,
handle: &mut TabletToolInnerHandle<'_, State>,
time: InputTime,
) {
handle.frame(data, time);
}
fn unset(&mut self, data: &mut State) {
if self.tip_down {
self.pointer_button(data, MouseButton::Left, ButtonState::Released);
}
let buttons = std::mem::take(&mut self.button_down);
for button in buttons.into_iter() {
if let Some(button) = self.button_to_mouse(button) {
self.pointer_button(data, button, ButtonState::Released);
}
}
}
fn start_data(&self) -> &GrabStartData<State> {
&self.start_data
}
}

View file

@ -33,8 +33,8 @@ use smithay::{
Tablet, Tablet,
tool::{ tool::{
AxisFrame as ToolAxisFrame, ButtonEvent as ToolButtonEvent, AxisFrame as ToolAxisFrame, ButtonEvent as ToolButtonEvent,
DownEvent as ToolDownEvent, MotionEvent as ToolMotionEvent, TabletToolTarget, DownEvent as ToolDownEvent, MotionEvent as ToolMotionEvent, TabletToolHandle,
UpEvent as ToolUpEvent, TabletToolTarget, UpEvent as ToolUpEvent,
}, },
}, },
touch::{ touch::{
@ -306,6 +306,16 @@ impl PointerFocusTarget {
session.set_cursor_hotspot(cursor_hotspot); session.set_cursor_hotspot(cursor_hotspot);
} }
} }
pub fn supports_tool(&self, tool_handle: &TabletToolHandle<State>) -> bool {
match self {
Self::WlSurface { surface, .. } if surface.client().is_some() => tool_handle
.client_tools(&surface.client().unwrap())
.next()
.is_some(),
_ => true,
}
}
} }
impl KeyboardFocusTarget { impl KeyboardFocusTarget {