diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers/mod.rs | 69 | ||||
| -rw-r--r-- | src/input/backend_ext.rs | 51 | ||||
| -rw-r--r-- | src/input/mod.rs | 151 | ||||
| -rw-r--r-- | src/niri.rs | 17 | ||||
| -rw-r--r-- | src/protocols/mod.rs | 1 | ||||
| -rw-r--r-- | src/protocols/virtual_pointer.rs | 563 |
6 files changed, 831 insertions, 21 deletions
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 9d147df4..9ffe5bab 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -11,7 +11,7 @@ use std::time::Duration; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::drm::DrmNode; -use smithay::backend::input::TabletToolDescriptor; +use smithay::backend::input::{InputEvent, TabletToolDescriptor}; use smithay::desktop::{PopupKind, PopupManager}; use smithay::input::pointer::{ CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle, @@ -35,6 +35,9 @@ use smithay::wayland::fractional_scale::FractionalScaleHandler; use smithay::wayland::idle_inhibit::IdleInhibitHandler; use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState}; use smithay::wayland::input_method::{InputMethodHandler, PopupSurface}; +use smithay::wayland::keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, +}; use smithay::wayland::output::OutputHandler; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler}; use smithay::wayland::security_context::{ @@ -59,11 +62,12 @@ use smithay::wayland::xdg_activation::{ use smithay::{ delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf, delegate_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify, - delegate_input_method_manager, delegate_output, delegate_pointer_constraints, - delegate_pointer_gestures, delegate_presentation, delegate_primary_selection, - delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock, - delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager, - delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation, + delegate_input_method_manager, delegate_keyboard_shortcuts_inhibit, delegate_output, + delegate_pointer_constraints, delegate_pointer_gestures, delegate_presentation, + delegate_primary_selection, delegate_relative_pointer, delegate_seat, + delegate_security_context, delegate_session_lock, delegate_single_pixel_buffer, + delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter, + delegate_virtual_keyboard_manager, delegate_xdg_activation, }; pub use crate::handlers::xdg_shell::KdeDecorationsModeState; @@ -75,10 +79,15 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt use crate::protocols::mutter_x11_interop::MutterX11InteropHandler; use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState}; use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState}; +use crate::protocols::virtual_pointer::{ + VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler, + VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent, + VirtualPointerMotionEvent, +}; use crate::utils::{output_size, send_scale_transform, with_toplevel_role}; use crate::{ delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop, - delegate_output_management, delegate_screencopy, + delegate_output_management, delegate_screencopy, delegate_virtual_pointer, }; pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10); @@ -243,7 +252,28 @@ impl InputMethodHandler for State { } } +impl KeyboardShortcutsInhibitHandler for State { + fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState { + &mut self.niri.keyboard_shortcuts_inhibit_state + } + + fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) { + // FIXME: show a confirmation dialog with a "remember for this application" kind of toggle. + inhibitor.activate(); + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .insert(inhibitor.wl_surface().clone(), inhibitor); + } + + fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) { + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .remove(&inhibitor.wl_surface().clone()); + } +} + delegate_input_method_manager!(State); +delegate_keyboard_shortcuts_inhibit!(State); delegate_virtual_keyboard_manager!(State); impl SelectionHandler for State { @@ -562,6 +592,31 @@ impl ScreencopyHandler for State { } delegate_screencopy!(State); +impl VirtualPointerHandler for State { + fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState { + &mut self.niri.virtual_pointer_state + } + + fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) { + self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event }); + } + + fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) { + self.process_input_event( + InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event }, + ); + } + + fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) { + self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event }); + } + + fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) { + self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event }); + } +} +delegate_virtual_pointer!(State); + impl DrmLeaseHandler for State { fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState { self.backend diff --git a/src/input/backend_ext.rs b/src/input/backend_ext.rs new file mode 100644 index 00000000..99f4a904 --- /dev/null +++ b/src/input/backend_ext.rs @@ -0,0 +1,51 @@ +use ::input as libinput; +use smithay::backend::input; +use smithay::backend::winit::WinitVirtualDevice; +use smithay::output::Output; + +use crate::niri::State; +use crate::protocols::virtual_pointer::VirtualPointer; + +pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> { + type NiriDevice: NiriInputDevice; +} +impl<T: input::InputBackend> NiriInputBackend for T +where + Self::Device: NiriInputDevice, +{ + type NiriDevice = Self::Device; +} + +pub trait NiriInputDevice: input::Device { + // FIXME: this should maybe be per-event, not per-device, + // but it's not clear that this matters in practice? + // it might be more obvious once we implement it for libinput + fn output(&self, state: &State) -> Option<Output>; +} + +impl NiriInputDevice for libinput::Device { + fn output(&self, _state: &State) -> Option<Output> { + // FIXME: Allow specifying the output per-device? + None + } +} + +impl NiriInputDevice for WinitVirtualDevice { + fn output(&self, _state: &State) -> Option<Output> { + // FIXME: we should be returning the single output that the winit backend creates, + // but for now, that will cause issues because the output is normally upside down, + // so we apply Transform::Flipped180 to it and that would also cause + // the cursor position to be flipped, which is not what we want. + // + // instead, we just return None and rely on the fact that it has only one output. + // doing so causes the cursor to be placed in *global* output coordinates, + // which are not flipped, and happen to be what we want. + None + } +} + +impl NiriInputDevice for VirtualPointer { + fn output(&self, _: &State) -> Option<Output> { + self.output().cloned() + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs index b08fe380..8872df78 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -11,7 +11,7 @@ use niri_ipc::LayoutSwitchTarget; use smithay::backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event, GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _, - InputBackend, InputEvent, KeyState, KeyboardKeyEvent, Keycode, MouseButton, PointerAxisEvent, + InputEvent, KeyState, KeyboardKeyEvent, Keycode, MouseButton, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, ProximityState, Switch, SwitchState, SwitchToggleEvent, TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, @@ -28,7 +28,9 @@ use smithay::input::touch::{ DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, UpEvent, }; use smithay::input::SeatHandler; +use smithay::output::Output; use smithay::utils::{Logical, Point, Rectangle, Transform, SERIAL_COUNTER}; +use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint}; use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait}; use touch_move_grab::TouchMoveGrab; @@ -42,6 +44,7 @@ use crate::ui::screenshot_ui::ScreenshotUi; use crate::utils::spawning::spawn; use crate::utils::{center, get_monotonic_time, ResizeEdge}; +pub mod backend_ext; pub mod move_grab; pub mod resize_grab; pub mod scroll_tracker; @@ -50,6 +53,8 @@ pub mod swipe_tracker; pub mod touch_move_grab; pub mod touch_resize_grab; +use backend_ext::{NiriInputBackend as InputBackend, NiriInputDevice as _}; + pub const DOUBLE_CLICK_TIME: Duration = Duration::from_millis(400); #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -264,8 +269,10 @@ impl State { where I::Device: 'static, { + let device_output = event.device().output(self); + let device_output = device_output.as_ref(); let (target_geo, keep_ratio, px, transform) = - if let Some(output) = self.niri.output_for_tablet() { + if let Some(output) = device_output.or_else(|| self.niri.output_for_tablet()) { ( self.niri.global_space.output_geometry(output).unwrap(), true, @@ -318,6 +325,18 @@ impl State { Some(pos + target_geo.loc.to_f64()) } + fn is_inhibiting_shortcuts(&self) -> bool { + self.niri + .keyboard_focus + .surface() + .and_then(|surface| { + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .get(surface) + }) + .is_some_and(KeyboardShortcutsInhibitor::is_active) + } + fn on_keyboard<I: InputBackend>(&mut self, event: I::KeyboardKeyEvent) { let comp_mod = self.backend.mod_key(); @@ -342,6 +361,8 @@ impl State { self.hide_cursor_if_needed(); } + let is_inhibiting_shortcuts = self.is_inhibiting_shortcuts(); + let Some(Some(bind)) = self.niri.seat.get_keyboard().unwrap().input( self, event.key_code(), @@ -372,6 +393,7 @@ impl State { *mods, &this.niri.screenshot_ui, this.niri.config.borrow().input.disable_power_key_handling, + is_inhibiting_shortcuts, ) }, ) else { @@ -610,6 +632,19 @@ impl State { }); } } + Action::ToggleKeyboardShortcutsInhibit => { + if let Some(inhibitor) = self.niri.keyboard_focus.surface().and_then(|surface| { + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .get(surface) + }) { + if inhibitor.is_active() { + inhibitor.inactivate(); + } else { + inhibitor.activate(); + } + } + } Action::CloseWindow => { if let Some(mapped) = self.niri.layout.focus() { mapped.toplevel().send_close(); @@ -1731,12 +1766,14 @@ impl State { &mut self, event: I::PointerMotionAbsoluteEvent, ) { - let Some(output_geo) = self.global_bounding_rectangle() else { + let Some(pos) = self.compute_absolute_location(&event, None).or_else(|| { + self.global_bounding_rectangle().map(|output_geo| { + event.position_transformed(output_geo.size) + output_geo.loc.to_f64() + }) + }) else { return; }; - let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64(); - let serial = SERIAL_COUNTER.next_serial(); let pointer = self.niri.seat.get_pointer().unwrap(); @@ -2613,14 +2650,13 @@ impl State { ); } - /// Computes the cursor position for the touch event. - /// - /// This function handles the touch output mapping, as well as coordinate transform - fn compute_touch_location<I: InputBackend, E: AbsolutePositionEvent<I>>( + fn compute_absolute_location<I: InputBackend>( &self, - evt: &E, + evt: &impl AbsolutePositionEvent<I>, + fallback_output: Option<&Output>, ) -> Option<Point<f64, Logical>> { - let output = self.niri.output_for_touch()?; + let output = evt.device().output(self); + let output = output.as_ref().or(fallback_output)?; let output_geo = self.niri.global_space.output_geometry(output).unwrap(); let transform = output.current_transform(); let size = transform.invert().transform_size(output_geo.size); @@ -2630,6 +2666,16 @@ impl State { ) } + /// Computes the cursor position for the touch event. + /// + /// This function handles the touch output mapping, as well as coordinate transform + fn compute_touch_location<I: InputBackend>( + &self, + evt: &impl AbsolutePositionEvent<I>, + ) -> Option<Point<f64, Logical>> { + self.compute_absolute_location(evt, self.niri.output_for_touch()) + } + fn on_touch_down<I: InputBackend>(&mut self, evt: I::TouchDownEvent) { let Some(handle) = self.niri.seat.get_touch() else { return; @@ -2780,6 +2826,7 @@ fn should_intercept_key( mods: ModifiersState, screenshot_ui: &ScreenshotUi, disable_power_key_handling: bool, + is_inhibiting_shortcuts: bool, ) -> FilterResult<Option<Bind>> { // Actions are only triggered on presses, release of the key // shouldn't try to intercept anything unless we have marked @@ -2820,6 +2867,10 @@ fn should_intercept_key( repeat: true, cooldown: None, allow_when_locked: false, + // The screenshot UI owns the focus anyway, so this doesn't really matter. + // But logically, nothing can inhibit its actions. Only opening it can be + // inhibited. + allow_inhibiting: false, }); } } @@ -2827,10 +2878,19 @@ fn should_intercept_key( match (final_bind, pressed) { (Some(bind), true) => { - suppressed_keys.insert(key_code); - FilterResult::Intercept(Some(bind)) + if is_inhibiting_shortcuts && bind.allow_inhibiting { + FilterResult::Forward + } else { + suppressed_keys.insert(key_code); + FilterResult::Intercept(Some(bind)) + } } (_, false) => { + // By this point, we know that the key was supressed on press. Even if we're inhibiting + // shortcuts, we should still suppress the release. + // But we don't need to check for shortcuts inhibition here, because + // if it was inhibited on press (forwarded to the client), it wouldn't be suppressed, + // so the release would already have been forwarded at the start of this function. suppressed_keys.remove(&key_code); FilterResult::Intercept(None) } @@ -2870,6 +2930,12 @@ fn find_bind( repeat: true, cooldown: None, allow_when_locked: false, + // In a worst-case scenario, the user has no way to unlock the compositor and a + // misbehaving client has a keyboard shortcuts inhibitor, "jailing" the user. + // The user must always be able to change VTs to recover from such a situation. + // It also makes no sense to inhibit the default power key handling. + // Hardcoded binds must never be inhibited. + allow_inhibiting: false, }); } @@ -3035,6 +3101,7 @@ fn allowed_when_locked(action: &Action) -> bool { | Action::PowerOffMonitors | Action::PowerOnMonitors | Action::SwitchLayout(_) + | Action::ToggleKeyboardShortcutsInhibit ) } @@ -3317,6 +3384,8 @@ pub fn mods_with_finger_scroll_binds(comp_mod: CompositorMod, binds: &Binds) -> #[cfg(test)] mod tests { + use std::cell::Cell; + use super::*; use crate::animation::Clock; @@ -3332,6 +3401,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }]); let comp_mod = CompositorMod::Super; @@ -3339,6 +3409,7 @@ mod tests { let screenshot_ui = ScreenshotUi::new(Clock::default(), Default::default()); let disable_power_key_handling = false; + let is_inhibiting_shortcuts = Cell::new(false); // The key_code we pick is arbitrary, the only thing // that matters is that they are different between cases. @@ -3356,6 +3427,7 @@ mod tests { mods, &screenshot_ui, disable_power_key_handling, + is_inhibiting_shortcuts.get(), ) }; @@ -3372,6 +3444,7 @@ mod tests { mods, &screenshot_ui, disable_power_key_handling, + is_inhibiting_shortcuts.get(), ) }; @@ -3452,6 +3525,53 @@ mod tests { // Ensure that no keys are being suppressed. assert!(suppressed_keys.is_empty()); + + // Now test shortcut inhibiting. + + // With inhibited shortcuts, we don't intercept our shortcut. + is_inhibiting_shortcuts.set(true); + + mods = ModifiersState { + logo: true, + ctrl: true, + ..Default::default() + }; + + let filter = close_key_event(&mut suppressed_keys, mods, true); + assert!(matches!(filter, FilterResult::Forward)); + assert!(suppressed_keys.is_empty()); + + let filter = close_key_event(&mut suppressed_keys, mods, false); + assert!(matches!(filter, FilterResult::Forward)); + assert!(suppressed_keys.is_empty()); + + // Toggle it off after pressing the shortcut. + let filter = close_key_event(&mut suppressed_keys, mods, true); + assert!(matches!(filter, FilterResult::Forward)); + assert!(suppressed_keys.is_empty()); + + is_inhibiting_shortcuts.set(false); + + let filter = close_key_event(&mut suppressed_keys, mods, false); + assert!(matches!(filter, FilterResult::Forward)); + assert!(suppressed_keys.is_empty()); + + // Toggle it on after pressing the shortcut. + let filter = close_key_event(&mut suppressed_keys, mods, true); + assert!(matches!( + filter, + FilterResult::Intercept(Some(Bind { + action: Action::CloseWindow, + .. + })) + )); + assert!(suppressed_keys.contains(&close_key_code)); + + is_inhibiting_shortcuts.set(true); + + let filter = close_key_event(&mut suppressed_keys, mods, false); + assert!(matches!(filter, FilterResult::Intercept(None))); + assert!(suppressed_keys.is_empty()); } #[test] @@ -3466,6 +3586,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3476,6 +3597,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3486,6 +3608,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3496,6 +3619,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3506,6 +3630,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, ]); diff --git a/src/niri.rs b/src/niri.rs index 16758ab5..c232ab88 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -77,6 +77,9 @@ use smithay::wayland::fractional_scale::FractionalScaleManagerState; use smithay::wayland::idle_inhibit::IdleInhibitManagerState; use smithay::wayland::idle_notify::IdleNotifierState; use smithay::wayland::input_method::{InputMethodManagerState, InputMethodSeat}; +use smithay::wayland::keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, +}; use smithay::wayland::output::OutputManagerState; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsState}; use smithay::wayland::pointer_gestures::PointerGesturesState; @@ -131,6 +134,7 @@ use crate::protocols::gamma_control::GammaControlManagerState; use crate::protocols::mutter_x11_interop::MutterX11InteropManagerState; use crate::protocols::output_management::OutputManagementManagerState; use crate::protocols::screencopy::{Screencopy, ScreencopyBuffer, ScreencopyManagerState}; +use crate::protocols::virtual_pointer::VirtualPointerManagerState; use crate::pw_utils::{Cast, PipeWire}; #[cfg(feature = "xdp-gnome-screencast")] use crate::pw_utils::{CastSizeChange, CastTarget, PwToNiri}; @@ -252,7 +256,9 @@ pub struct Niri { pub tablet_state: TabletManagerState, pub text_input_state: TextInputManagerState, pub input_method_state: InputMethodManagerState, + pub keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState, pub virtual_keyboard_state: VirtualKeyboardManagerState, + pub virtual_pointer_state: VirtualPointerManagerState, pub pointer_gestures_state: PointerGesturesState, pub relative_pointer_state: RelativePointerManagerState, pub pointer_constraints_state: PointerConstraintsState, @@ -290,6 +296,7 @@ pub struct Niri { pub previously_focused_window: Option<Window>, pub idle_inhibiting_surfaces: HashSet<WlSurface>, pub is_fdo_idle_inhibited: Arc<AtomicBool>, + pub keyboard_shortcuts_inhibiting_surfaces: HashMap<WlSurface, KeyboardShortcutsInhibitor>, pub cursor_manager: CursorManager, pub cursor_texture_cache: CursorTextureCache, @@ -1818,11 +1825,16 @@ impl Niri { InputMethodManagerState::new::<State, _>(&display_handle, |client| { !client.get_data::<ClientState>().unwrap().restricted }); + let keyboard_shortcuts_inhibit_state = + KeyboardShortcutsInhibitState::new::<State>(&display_handle); let virtual_keyboard_state = VirtualKeyboardManagerState::new::<State, _>(&display_handle, |client| { !client.get_data::<ClientState>().unwrap().restricted }); - + let virtual_pointer_state = + VirtualPointerManagerState::new::<State, _>(&display_handle, |client| { + !client.get_data::<ClientState>().unwrap().restricted + }); let foreign_toplevel_state = ForeignToplevelManagerState::new::<State, _>(&display_handle, |client| { !client.get_data::<ClientState>().unwrap().restricted @@ -2014,7 +2026,9 @@ impl Niri { xdg_foreign_state, text_input_state, input_method_state, + keyboard_shortcuts_inhibit_state, virtual_keyboard_state, + virtual_pointer_state, shm_state, output_manager_state, dmabuf_state, @@ -2049,6 +2063,7 @@ impl Niri { previously_focused_window: None, idle_inhibiting_surfaces: HashSet::new(), is_fdo_idle_inhibited: Arc::new(AtomicBool::new(false)), + keyboard_shortcuts_inhibiting_surfaces: HashMap::new(), cursor_manager, cursor_texture_cache: Default::default(), cursor_shape_manager_state, diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 3328fb7c..476f24eb 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -3,5 +3,6 @@ pub mod gamma_control; pub mod mutter_x11_interop; pub mod output_management; pub mod screencopy; +pub mod virtual_pointer; pub mod raw; diff --git a/src/protocols/virtual_pointer.rs b/src/protocols/virtual_pointer.rs new file mode 100644 index 00000000..ff3cb3e9 --- /dev/null +++ b/src/protocols/virtual_pointer.rs @@ -0,0 +1,563 @@ +use std::collections::HashSet; +use std::sync::Mutex; + +use smithay::backend::input::{ + AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, ButtonState, Device, + DeviceCapability, Event, InputBackend, PointerAxisEvent, PointerButtonEvent, + PointerMotionAbsoluteEvent, PointerMotionEvent, UnusedEvent, +}; +use smithay::input::pointer::AxisFrame; +use smithay::output::Output; +use smithay::reexports::wayland_protocols_wlr; +use smithay::reexports::wayland_server::protocol::wl_pointer; +use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; +use wayland_backend::protocol::WEnum; +use wayland_protocols_wlr::virtual_pointer::v1::server::{ + zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1, +}; +use zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1; +use zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1; + +const VERSION: u32 = 2; + +pub struct VirtualPointerManagerState { + virtual_pointers: HashSet<ZwlrVirtualPointerV1>, +} + +pub struct VirtualPointerManagerGlobalData { + filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>, +} + +pub struct VirtualPointerInputBackend; + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct VirtualPointer { + pointer: ZwlrVirtualPointerV1, +} + +#[derive(Debug)] +pub struct VirtualPointerUserData { + seat: Option<WlSeat>, + output: Option<Output>, + + axis_frame: Mutex<Option<AxisFrame>>, +} + +impl VirtualPointer { + fn data(&self) -> &VirtualPointerUserData { + self.pointer.data().unwrap() + } + + pub fn seat(&self) -> Option<&WlSeat> { + self.data().seat.as_ref() + } + + pub fn output(&self) -> Option<&Output> { + self.data().output.as_ref() + } + + fn finish_axis_frame(&self) -> Option<AxisFrame> { + self.data().axis_frame.lock().unwrap().take() + } + + fn mutate_axis_frame(&self, time: Option<u32>, f: impl FnOnce(AxisFrame) -> AxisFrame) { + let mut frame = self.data().axis_frame.lock().unwrap(); + + *frame = frame.or(time.map(AxisFrame::new)).map(f); + } +} + +impl Device for VirtualPointer { + fn id(&self) -> String { + format!("wlr virtual pointer {}", self.pointer.id()) + } + + fn name(&self) -> String { + String::from("virtual pointer") + } + + fn has_capability(&self, capability: DeviceCapability) -> bool { + matches!(capability, DeviceCapability::Pointer) + } + + fn usb_id(&self) -> Option<(u32, u32)> { + None + } + + fn syspath(&self) -> Option<std::path::PathBuf> { + None + } +} + +pub struct VirtualPointerMotionEvent { + pointer: VirtualPointer, + time: u32, + dx: f64, + dy: f64, +} + +impl Event<VirtualPointerInputBackend> for VirtualPointerMotionEvent { + fn time(&self) -> u64 { + self.time as u64 * 1000 // millis to micros + } + + fn device(&self) -> VirtualPointer { + self.pointer.clone() + } +} + +impl PointerMotionEvent<VirtualPointerInputBackend> for VirtualPointerMotionEvent { + fn delta_x(&self) -> f64 { + self.dx + } + + fn delta_y(&self) -> f64 { + self.dy + } + + fn delta_x_unaccel(&self) -> f64 { + self.dx + } + + fn delta_y_unaccel(&self) -> f64 { + self.dy + } +} + +pub struct VirtualPointerMotionAbsoluteEvent { + pointer: VirtualPointer, + time: u32, + x: u32, + y: u32, + x_extent: u32, + y_extent: u32, +} + +impl Event<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent { + fn time(&self) -> u64 { + self.time as u64 * 1000 // millis to micros + } + + fn device(&self) -> VirtualPointer { + self.pointer.clone() + } +} + +impl AbsolutePositionEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent { + fn x(&self) -> f64 { + self.x as f64 / self.x_extent as f64 + } + + fn y(&self) -> f64 { + self.y as f64 / self.y_extent as f64 + } + + fn x_transformed(&self, width: i32) -> f64 { + (self.x as i64 * width as i64) as f64 / self.x_extent as f64 + } + + fn y_transformed(&self, height: i32) -> f64 { + (self.y as i64 * height as i64) as f64 / self.y_extent as f64 + } +} + +pub struct VirtualPointerButtonEvent { + pointer: VirtualPointer, + time: u32, + button: u32, + state: ButtonState, +} + +impl Event<VirtualPointerInputBackend> for VirtualPointerButtonEvent { + fn time(&self) -> u64 { + self.time as u64 * 1000 // millis to micros + } + + fn device(&self) -> VirtualPointer { + self.pointer.clone() + } +} + +impl PointerButtonEvent<VirtualPointerInputBackend> for VirtualPointerButtonEvent { + fn button_code(&self) -> u32 { + self.button + } + + fn state(&self) -> ButtonState { + self.state + } +} + +pub struct VirtualPointerAxisEvent { + pointer: VirtualPointer, + frame: AxisFrame, +} + +impl Event<VirtualPointerInputBackend> for VirtualPointerAxisEvent { + fn time(&self) -> u64 { + self.frame.time as u64 * 1000 // millis to micros + } + + fn device(&self) -> VirtualPointer { + self.pointer.clone() + } +} + +fn tuple_axis<T>(tuple: (T, T), axis: Axis) -> T { + match axis { + Axis::Horizontal => tuple.0, + Axis::Vertical => tuple.1, + } +} + +impl PointerAxisEvent<VirtualPointerInputBackend> for VirtualPointerAxisEvent { + fn amount(&self, axis: Axis) -> Option<f64> { + Some(tuple_axis(self.frame.axis, axis)) + } + + fn amount_v120(&self, axis: Axis) -> Option<f64> { + self.frame.v120.map(|v120| tuple_axis(v120, axis) as f64) + } + + fn source(&self) -> AxisSource { + self.frame.source.unwrap_or_else(|| { + warn!("AxisSource: no source set, giving bogus value"); + AxisSource::Continuous + }) + } + + fn relative_direction(&self, axis: Axis) -> AxisRelativeDirection { + tuple_axis(self.frame.relative_direction, axis) + } +} + +impl PointerMotionAbsoluteEvent<VirtualPointerInputBackend> for VirtualPointerMotionAbsoluteEvent {} + +impl InputBackend for VirtualPointerInputBackend { + type Device = VirtualPointer; + + type KeyboardKeyEvent = UnusedEvent; + type PointerAxisEvent = VirtualPointerAxisEvent; + type PointerButtonEvent = VirtualPointerButtonEvent; + type PointerMotionEvent = VirtualPointerMotionEvent; + type PointerMotionAbsoluteEvent = VirtualPointerMotionAbsoluteEvent; + + type GestureSwipeBeginEvent = UnusedEvent; + type GestureSwipeUpdateEvent = UnusedEvent; + type GestureSwipeEndEvent = UnusedEvent; + type GesturePinchBeginEvent = UnusedEvent; + type GesturePinchUpdateEvent = UnusedEvent; + type GesturePinchEndEvent = UnusedEvent; + type GestureHoldBeginEvent = UnusedEvent; + type GestureHoldEndEvent = UnusedEvent; + + type TouchDownEvent = UnusedEvent; + type TouchUpEvent = UnusedEvent; + type TouchMotionEvent = UnusedEvent; + type TouchCancelEvent = UnusedEvent; + type TouchFrameEvent = UnusedEvent; + type TabletToolAxisEvent = UnusedEvent; + type TabletToolProximityEvent = UnusedEvent; + type TabletToolTipEvent = UnusedEvent; + type TabletToolButtonEvent = UnusedEvent; + + type SwitchToggleEvent = UnusedEvent; + + type SpecialEvent = UnusedEvent; +} + +pub trait VirtualPointerHandler { + fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState; + + fn create_virtual_pointer(&mut self, pointer: VirtualPointer) { + let _ = pointer; + } + fn destroy_virtual_pointer(&mut self, pointer: VirtualPointer) { + let _ = pointer; + } + + fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent); + fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent); + fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent); + fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent); +} + +impl VirtualPointerManagerState { + pub fn new<D, F>(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>, + D: Dispatch<ZwlrVirtualPointerManagerV1, ()>, + D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>, + D: VirtualPointerHandler, + D: 'static, + F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, + { + let global_data = VirtualPointerManagerGlobalData { + filter: Box::new(filter), + }; + display.create_global::<D, ZwlrVirtualPointerManagerV1, _>(VERSION, global_data); + + Self { + virtual_pointers: HashSet::new(), + } + } +} + +impl<D> GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData, D> + for VirtualPointerManagerState +where + D: GlobalDispatch<ZwlrVirtualPointerManagerV1, VirtualPointerManagerGlobalData>, + D: Dispatch<ZwlrVirtualPointerManagerV1, ()>, + D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>, + D: VirtualPointerHandler, + D: 'static, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + manager: New<ZwlrVirtualPointerManagerV1>, + _manager_state: &VirtualPointerManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(manager, ()); + } + + fn can_view(client: Client, global_data: &VirtualPointerManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl<D> Dispatch<ZwlrVirtualPointerManagerV1, (), D> for VirtualPointerManagerState +where + D: Dispatch<ZwlrVirtualPointerManagerV1, ()>, + D: Dispatch<ZwlrVirtualPointerV1, VirtualPointerUserData>, + D: VirtualPointerHand |
