diff --git a/src/input/mod.rs b/src/input/mod.rs index cbf99cae..28877356 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -9,7 +9,10 @@ use crate::{ cosmic_modifiers_from_smithay, }, }, - input::gestures::{GestureState, SwipeAction}, + input::{ + gestures::{GestureState, SwipeAction}, + tablet_emu::PointerEmulationGrab, + }, shell::{ SeatExt, Trigger, focus::{ @@ -52,7 +55,7 @@ use smithay::{ keyboard::KeyboardHandle, keyboard::{FilterResult, KeyboardSource, KeysymHandle, ModifiersState}, pointer::{ - AxisFrame, ButtonEvent as PointerButtonEvent, GestureHoldBeginEvent, + AxisFrame, ButtonEvent as PointerButtonEvent, Focus, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent as PointerMotionEvent, PointerGrab, PointerHandle, @@ -89,6 +92,7 @@ use std::{ pub mod actions; pub mod gestures; +pub mod tablet_emu; /// Identifies the input backend instance an event came from, used to disambiguate device ids /// (which are only unique within a single backend instance, see @@ -1609,6 +1613,26 @@ impl State { let tool = tablet_seat.get_tool(&event.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 { pressure: event.pressure_has_changed().then(|| event.pressure()), distance: event.distance_has_changed().then(|| event.distance()), @@ -1629,7 +1653,7 @@ impl State { under, &tool::MotionEvent { location: position.as_logical(), - serial: SERIAL_COUNTER.next_serial(), + serial, time: event.time(), }, ); @@ -1683,6 +1707,25 @@ impl State { if let Some(tablet) = tablet { 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 { pressure: event.pressure_has_changed().then(|| event.pressure()), distance: event.distance_has_changed().then(|| event.distance()), @@ -1697,24 +1740,39 @@ impl State { }; match event.state() { - ProximityState::In => tool.proximity_in( - self, - under, - tablet, - &tool::ProximityInEvent { - location: position.as_logical(), - axis: Some(frame), - serial: SERIAL_COUNTER.next_serial(), - time: event.time(), - }, - ), - ProximityState::Out => tool.proximity_out( - self, - &tool::ProximityOutEvent { - serial, - time: event.time(), - }, - ), + ProximityState::In => { + tool.proximity_in( + self, + under, + tablet, + &tool::ProximityInEvent { + location: position.as_logical(), + axis: Some(frame), + serial, + time: event.time(), + }, + ); + } + ProximityState::Out => { + tool.proximity_out( + 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()); diff --git a/src/input/tablet_emu.rs b/src/input/tablet_emu.rs new file mode 100644 index 00000000..94d31d89 --- /dev/null +++ b/src/input/tablet_emu.rs @@ -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, + seat: Seat, + + above_source: bool, + tip_down: bool, + button_down: Vec, +} + +impl PointerEmulationGrab { + pub fn new(start_data: GrabStartData, seat: Seat) -> Self { + Self { + start_data, + seat, + + above_source: true, + tip_down: false, + button_down: Vec::new(), + } + } + + fn button_to_mouse(&self, button: u32) -> Option { + // 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 for PointerEmulationGrab { + fn proximity_in( + &mut self, + data: &mut State, + handle: &mut TabletToolInnerHandle<'_, State>, + focus: Option<(PointerFocusTarget, Point)>, + 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)>, + 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 { + &self.start_data + } +} diff --git a/src/shell/focus/target.rs b/src/shell/focus/target.rs index 6ca13279..c914e0fa 100644 --- a/src/shell/focus/target.rs +++ b/src/shell/focus/target.rs @@ -33,8 +33,8 @@ use smithay::{ Tablet, tool::{ AxisFrame as ToolAxisFrame, ButtonEvent as ToolButtonEvent, - DownEvent as ToolDownEvent, MotionEvent as ToolMotionEvent, TabletToolTarget, - UpEvent as ToolUpEvent, + DownEvent as ToolDownEvent, MotionEvent as ToolMotionEvent, TabletToolHandle, + TabletToolTarget, UpEvent as ToolUpEvent, }, }, touch::{ @@ -306,6 +306,16 @@ impl PointerFocusTarget { session.set_cursor_hotspot(cursor_hotspot); } } + + pub fn supports_tool(&self, tool_handle: &TabletToolHandle) -> bool { + match self { + Self::WlSurface { surface, .. } if surface.client().is_some() => tool_handle + .client_tools(&surface.client().unwrap()) + .next() + .is_some(), + _ => true, + } + } } impl KeyboardFocusTarget {