From 0dade287ce3e58feb9e4af002459c0f799aaff7e Mon Sep 17 00:00:00 2001 From: sodiboo Date: Sun, 25 Aug 2024 03:54:51 +0200 Subject: [PATCH 1/7] stub keyboard-shortcuts-inhibit and virtual-pointer impls --- src/handlers/mod.rs | 39 ++++++- src/niri.rs | 13 ++- src/protocols/mod.rs | 1 + src/protocols/virtual_pointer.rs | 193 +++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 src/protocols/virtual_pointer.rs diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 9d147df4b..69941932f 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -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,11 @@ 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::{VirtualPointerHandler, VirtualPointerManagerState}; 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 +248,22 @@ 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) { + info!("new inhibitor: {inhibitor:?}"); + } + + fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) { + info!("inhibitor destroyed: {inhibitor:?}"); + } +} + delegate_input_method_manager!(State); +delegate_keyboard_shortcuts_inhibit!(State); delegate_virtual_keyboard_manager!(State); impl SelectionHandler for State { @@ -562,6 +582,13 @@ 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 + } +} +delegate_virtual_pointer!(State); + impl DrmLeaseHandler for State { fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState { self.backend diff --git a/src/niri.rs b/src/niri.rs index 88dbd0eeb..b237b3ea6 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -77,6 +77,7 @@ 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; use smithay::wayland::output::OutputManagerState; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsState}; use smithay::wayland::pointer_gestures::PointerGesturesState; @@ -131,6 +132,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 +254,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, @@ -1814,11 +1818,16 @@ impl Niri { InputMethodManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); + let keyboard_shortcuts_inhibit_state = + KeyboardShortcutsInhibitState::new::(&display_handle); let virtual_keyboard_state = VirtualKeyboardManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted }); - + let virtual_pointer_state = + VirtualPointerManagerState::new::(&display_handle, |client| { + !client.get_data::().unwrap().restricted + }); let foreign_toplevel_state = ForeignToplevelManagerState::new::(&display_handle, |client| { !client.get_data::().unwrap().restricted @@ -2010,7 +2019,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, diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 3328fb7cb..476f24eba 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 000000000..f192a38c8 --- /dev/null +++ b/src/protocols/virtual_pointer.rs @@ -0,0 +1,193 @@ +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; + +use smithay::output::Output; +use smithay::reexports::wayland_protocols_wlr; +use smithay::reexports::wayland_server::backend::ClientId; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; +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: Vec, +} + +pub struct VirtualPointerManagerGlobalData { + filter: Box Fn(&'c Client) -> bool + Send + Sync>, +} + +pub trait VirtualPointerHandler { + fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState; +} + +pub struct VirtualPointerState {} + +impl VirtualPointerManagerState { + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + 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::(VERSION, global_data); + + Self { + virtual_pointers: Vec::new(), + } + } +} + +impl GlobalDispatch + for VirtualPointerManagerState +where + D: GlobalDispatch, + D: Dispatch, + D: Dispatch, + D: VirtualPointerHandler, + D: 'static, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + manager: New, + _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 Dispatch for VirtualPointerManagerState +where + D: Dispatch, + D: Dispatch, + D: VirtualPointerHandler, + D: 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _resource: &ZwlrVirtualPointerManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointer { seat, id } => { + let virtual_pointer = data_init.init(id, VirtualPointerState {}); + state.virtual_pointer_manager_state().virtual_pointers.push(virtual_pointer); + } + zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointerWithOutput { + seat, + output, + id, + } => { + info!("CreateVirtualPointerWithOutput"); + let virtual_pointer = data_init.init(id, VirtualPointerState {}); + state.virtual_pointer_manager_state().virtual_pointers.push(virtual_pointer); + } + zwlr_virtual_pointer_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch for VirtualPointerManagerState +where + D: Dispatch, + D: VirtualPointerHandler, + D: 'static, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ZwlrVirtualPointerV1, + request: ::Request, + data: &VirtualPointerState, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_virtual_pointer_v1::Request::Motion { time, dx, dy } => { + info!("Motion: {dx},{dy}"); + } + zwlr_virtual_pointer_v1::Request::MotionAbsolute { + time, + x, + y, + x_extent, + y_extent, + } => { + info!("MotionAbsolute: {x}({x_extent}),{y}({y_extent})"); + } + zwlr_virtual_pointer_v1::Request::Button { + time, + button, + state, + } => { + info!("Button: {button} / {state:?}"); + } + zwlr_virtual_pointer_v1::Request::Axis { time, axis, value } => { + info!("Axis: {axis:?} / {value}"); + } + zwlr_virtual_pointer_v1::Request::Frame => { + info!("Frame"); + } + zwlr_virtual_pointer_v1::Request::AxisSource { axis_source } => { + info!("AxisSource: {axis_source:?}"); + } + zwlr_virtual_pointer_v1::Request::AxisStop { time, axis } => { + info!("AxisStop: {axis:?}"); + } + zwlr_virtual_pointer_v1::Request::AxisDiscrete { + time, + axis, + value, + discrete, + } => { + info!("AxisDiscrete: {axis:?} / {value} / {discrete}"); + } + zwlr_virtual_pointer_v1::Request::Destroy => { + info!("Destroy"); + } + _ => unreachable!(), + } + } +} + +#[macro_export] +macro_rules! delegate_virtual_pointer { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: $crate::protocols::virtual_pointer::VirtualPointerManagerGlobalData + ] => $crate::protocols::virtual_pointer::VirtualPointerManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: () + ] => $crate::protocols::virtual_pointer::VirtualPointerManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1: $crate::protocols::virtual_pointer::VirtualPointerState + ] => $crate::protocols::virtual_pointer::VirtualPointerManagerState); + }; +} From 132dc72b1bb3326833fcb4c5550c491f45827b66 Mon Sep 17 00:00:00 2001 From: sodiboo Date: Sun, 25 Aug 2024 15:20:41 +0200 Subject: [PATCH 2/7] implement keyboard-shortcuts-inhibit --- src/handlers/mod.rs | 10 ++++++++-- src/input/mod.rs | 31 +++++++++++++++++++++++++++++++ src/niri.rs | 4 +++- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 69941932f..dba0da9b5 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -254,11 +254,17 @@ impl KeyboardShortcutsInhibitHandler for State { } fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) { - info!("new inhibitor: {inhibitor:?}"); + // 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) { - info!("inhibitor destroyed: {inhibitor:?}"); + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .remove(&inhibitor.wl_surface().clone()); } } diff --git a/src/input/mod.rs b/src/input/mod.rs index ae2c2b056..58a711afa 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -29,6 +29,7 @@ use smithay::input::touch::{ }; use smithay::input::SeatHandler; 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; @@ -318,6 +319,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(&mut self, event: I::KeyboardKeyEvent) { let comp_mod = self.backend.mod_key(); @@ -342,6 +355,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 +387,7 @@ impl State { *mods, &this.niri.screenshot_ui, this.niri.config.borrow().input.disable_power_key_handling, + is_inhibiting_shortcuts, ) }, ) else { @@ -2773,6 +2789,7 @@ fn should_intercept_key( mods: ModifiersState, screenshot_ui: &ScreenshotUi, disable_power_key_handling: bool, + is_inhibiting_shortcuts: bool, ) -> FilterResult> { // Actions are only triggered on presses, release of the key // shouldn't try to intercept anything unless we have marked @@ -2818,6 +2835,18 @@ fn should_intercept_key( } } + if is_inhibiting_shortcuts + && !matches!( + final_bind, + Some(Bind { + action: Action::ChangeVt(_), + .. + }) + ) + { + return FilterResult::Forward; + } + match (final_bind, pressed) { (Some(bind), true) => { suppressed_keys.insert(key_code); @@ -3349,6 +3378,7 @@ mod tests { mods, &screenshot_ui, disable_power_key_handling, + false, ) }; @@ -3365,6 +3395,7 @@ mod tests { mods, &screenshot_ui, disable_power_key_handling, + false, ) }; diff --git a/src/niri.rs b/src/niri.rs index b237b3ea6..8be855cff 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -77,7 +77,7 @@ 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; +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; @@ -294,6 +294,7 @@ pub struct Niri { pub previously_focused_window: Option, pub idle_inhibiting_surfaces: HashSet, pub is_fdo_idle_inhibited: Arc, + pub keyboard_shortcuts_inhibiting_surfaces: HashMap, pub cursor_manager: CursorManager, pub cursor_texture_cache: CursorTextureCache, @@ -2056,6 +2057,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, From 38615a600576a87298a4eafb37751cc665175340 Mon Sep 17 00:00:00 2001 From: sodiboo Date: Mon, 26 Aug 2024 06:10:57 +0200 Subject: [PATCH 3/7] implement virtual-pointer --- src/handlers/mod.rs | 26 +- src/niri.rs | 4 +- src/protocols/virtual_pointer.rs | 448 ++++++++++++++++++++++++++++--- 3 files changed, 436 insertions(+), 42 deletions(-) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index dba0da9b5..9ffe5babc 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, @@ -79,7 +79,11 @@ 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::{VirtualPointerHandler, VirtualPointerManagerState}; +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, @@ -592,6 +596,24 @@ 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::::PointerMotion { event }); + } + + fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) { + self.process_input_event( + InputEvent::::PointerMotionAbsolute { event }, + ); + } + + fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) { + self.process_input_event(InputEvent::::PointerButton { event }); + } + + fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) { + self.process_input_event(InputEvent::::PointerAxis { event }); + } } delegate_virtual_pointer!(State); diff --git a/src/niri.rs b/src/niri.rs index 8be855cff..3bde9e6ef 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -77,7 +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::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; diff --git a/src/protocols/virtual_pointer.rs b/src/protocols/virtual_pointer.rs index f192a38c8..f55ddbb62 100644 --- a/src/protocols/virtual_pointer.rs +++ b/src/protocols/virtual_pointer.rs @@ -1,13 +1,20 @@ -use std::collections::HashMap; -use std::fs::File; -use std::io::Read; +use std::collections::HashSet; +use std::sync::Mutex; -use smithay::output::Output; +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::reexports::wayland_protocols_wlr; -use smithay::reexports::wayland_server::backend::ClientId; +use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; +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, }; @@ -17,25 +24,273 @@ use zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1; const VERSION: u32 = 2; pub struct VirtualPointerManagerState { - virtual_pointers: Vec, + virtual_pointers: HashSet, } pub struct VirtualPointerManagerGlobalData { filter: Box 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, + output: Option, + + axis_frame: Mutex>, +} + +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<&WlOutput> { + self.data().output.as_ref() + } + + fn finish_axis_frame(&self) -> Option { + self.data().axis_frame.lock().unwrap().take() + } + + fn mutate_axis_frame(&self, time: Option, 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 { + None + } +} + +pub struct VirtualPointerMotionEvent { + pointer: VirtualPointer, + time: u32, + dx: f64, + dy: f64, +} + +impl Event for VirtualPointerMotionEvent { + fn time(&self) -> u64 { + self.time as u64 * 1000 // millis to micros + } + + fn device(&self) -> VirtualPointer { + self.pointer.clone() + } +} + +impl PointerMotionEvent 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 for VirtualPointerMotionAbsoluteEvent { + fn time(&self) -> u64 { + self.time as u64 * 1000 // millis to micros + } + + fn device(&self) -> VirtualPointer { + self.pointer.clone() + } +} + +impl AbsolutePositionEvent 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 for VirtualPointerButtonEvent { + fn time(&self) -> u64 { + self.time as u64 * 1000 // millis to micros + } + + fn device(&self) -> VirtualPointer { + self.pointer.clone() + } +} + +impl PointerButtonEvent for VirtualPointerButtonEvent { + fn button_code(&self) -> u32 { + self.button + } + + fn state(&self) -> ButtonState { + self.state + } +} + +pub struct VirtualPointerAxisEvent { + pointer: VirtualPointer, + frame: AxisFrame, +} + +impl Event 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(tuple: (T, T), axis: Axis) -> T { + match axis { + Axis::Horizontal => tuple.0, + Axis::Vertical => tuple.1, + } +} + +impl PointerAxisEvent for VirtualPointerAxisEvent { + fn amount(&self, axis: Axis) -> Option { + Some(tuple_axis(self.frame.axis, axis)) + } + + fn amount_v120(&self, axis: Axis) -> Option { + 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 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; -} -pub struct VirtualPointerState {} + 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(display: &DisplayHandle, filter: F) -> Self where D: GlobalDispatch, D: Dispatch, - D: Dispatch, + D: Dispatch, D: VirtualPointerHandler, D: 'static, F: for<'c> Fn(&'c Client) -> bool + Send + Sync + 'static, @@ -46,7 +301,7 @@ impl VirtualPointerManagerState { display.create_global::(VERSION, global_data); Self { - virtual_pointers: Vec::new(), + virtual_pointers: HashSet::new(), } } } @@ -56,7 +311,7 @@ impl GlobalDispatch, D: Dispatch, - D: Dispatch, + D: Dispatch, D: VirtualPointerHandler, D: 'static, { @@ -79,7 +334,7 @@ where impl Dispatch for VirtualPointerManagerState where D: Dispatch, - D: Dispatch, + D: Dispatch, D: VirtualPointerHandler, D: 'static, { @@ -92,44 +347,63 @@ where _dhandle: &DisplayHandle, data_init: &mut DataInit<'_, D>, ) { - match request { + let (id, seat, output) = match request { zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointer { seat, id } => { - let virtual_pointer = data_init.init(id, VirtualPointerState {}); - state.virtual_pointer_manager_state().virtual_pointers.push(virtual_pointer); + (id, seat, None) } zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointerWithOutput { seat, output, id, - } => { - info!("CreateVirtualPointerWithOutput"); - let virtual_pointer = data_init.init(id, VirtualPointerState {}); - state.virtual_pointer_manager_state().virtual_pointers.push(virtual_pointer); - } - zwlr_virtual_pointer_manager_v1::Request::Destroy => {} + } => (id, seat, output), + zwlr_virtual_pointer_manager_v1::Request::Destroy => return, _ => unreachable!(), - } + }; + + let pointer = data_init.init( + id, + VirtualPointerUserData { + seat, + output, + axis_frame: Mutex::new(None), + }, + ); + state + .virtual_pointer_manager_state() + .virtual_pointers + .insert(pointer.clone()); + + state.create_virtual_pointer(VirtualPointer { pointer }); } } -impl Dispatch for VirtualPointerManagerState +impl Dispatch for VirtualPointerManagerState where - D: Dispatch, + D: Dispatch, D: VirtualPointerHandler, D: 'static, { fn request( - state: &mut D, + handler: &mut D, _client: &Client, resource: &ZwlrVirtualPointerV1, request: ::Request, - data: &VirtualPointerState, + _data: &VirtualPointerUserData, _dhandle: &DisplayHandle, _data_init: &mut DataInit<'_, D>, ) { + let pointer = VirtualPointer { + pointer: resource.clone(), + }; match request { zwlr_virtual_pointer_v1::Request::Motion { time, dx, dy } => { - info!("Motion: {dx},{dy}"); + let event = VirtualPointerMotionEvent { + pointer, + time, + dx, + dy, + }; + handler.on_virtual_pointer_motion(event); } zwlr_virtual_pointer_v1::Request::MotionAbsolute { time, @@ -138,26 +412,93 @@ where x_extent, y_extent, } => { - info!("MotionAbsolute: {x}({x_extent}),{y}({y_extent})"); + let event = VirtualPointerMotionAbsoluteEvent { + pointer, + time, + x, + y, + x_extent, + y_extent, + }; + handler.on_virtual_pointer_motion_absolute(event); } zwlr_virtual_pointer_v1::Request::Button { time, button, state, } => { - info!("Button: {button} / {state:?}"); + // state is an enum but wlroots treats it as a C boolean (zero or nonzero) + // so we emulate that behaviour too. ButtonState::Pressed and any invalid value + // counts as pressed. + // https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/3187479c07c34a4de82c06a316a763a36a0499da/types/wlr_virtual_pointer_v1.c#L74 + let state = match state { + WEnum::Value(wl_pointer::ButtonState::Released) => ButtonState::Released, + _ => ButtonState::Pressed, + }; + let event = VirtualPointerButtonEvent { + pointer, + time, + button, + state, + }; + handler.on_virtual_pointer_button(event); } zwlr_virtual_pointer_v1::Request::Axis { time, axis, value } => { - info!("Axis: {axis:?} / {value}"); + let axis = match axis { + WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical, + WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal, + _ => { + warn!("Axis: invalid axis"); + resource.post_error( + zwlr_virtual_pointer_v1::Error::InvalidAxis, + "invalid axis", + ); + return; + } + }; + + pointer.mutate_axis_frame(Some(time), |frame| frame.value(axis, value)); } zwlr_virtual_pointer_v1::Request::Frame => { - info!("Frame"); + if let Some(frame) = pointer.finish_axis_frame() { + let event = VirtualPointerAxisEvent { pointer, frame }; + handler.on_virtual_pointer_axis(event); + } } zwlr_virtual_pointer_v1::Request::AxisSource { axis_source } => { - info!("AxisSource: {axis_source:?}"); + let axis_source = match axis_source { + WEnum::Value(wl_pointer::AxisSource::Wheel) => AxisSource::Wheel, + WEnum::Value(wl_pointer::AxisSource::Finger) => AxisSource::Finger, + WEnum::Value(wl_pointer::AxisSource::Continuous) => AxisSource::Continuous, + WEnum::Value(wl_pointer::AxisSource::WheelTilt) => AxisSource::WheelTilt, + + _ => { + warn!("AxisSource: invalid axis source"); + resource.post_error( + zwlr_virtual_pointer_v1::Error::InvalidAxisSource, + "invalid axis source", + ); + return; + } + }; + + pointer.mutate_axis_frame(None, |frame| frame.source(axis_source)); } zwlr_virtual_pointer_v1::Request::AxisStop { time, axis } => { - info!("AxisStop: {axis:?}"); + let axis = match axis { + WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical, + WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal, + _ => { + warn!("AxisStop: invalid axis"); + resource.post_error( + zwlr_virtual_pointer_v1::Error::InvalidAxis, + "invalid axis", + ); + return; + } + }; + + pointer.mutate_axis_frame(Some(time), |frame| frame.stop(axis)); } zwlr_virtual_pointer_v1::Request::AxisDiscrete { time, @@ -165,14 +506,43 @@ where value, discrete, } => { - info!("AxisDiscrete: {axis:?} / {value} / {discrete}"); - } - zwlr_virtual_pointer_v1::Request::Destroy => { - info!("Destroy"); + let axis = match axis { + WEnum::Value(wl_pointer::Axis::VerticalScroll) => Axis::Vertical, + WEnum::Value(wl_pointer::Axis::HorizontalScroll) => Axis::Horizontal, + _ => { + warn!("AxisDiscrete: invalid axis"); + resource.post_error( + zwlr_virtual_pointer_v1::Error::InvalidAxis, + "invalid axis", + ); + return; + } + }; + pointer.mutate_axis_frame(Some(time), |frame| { + frame.value(axis, value).v120(axis, discrete) + }); } + zwlr_virtual_pointer_v1::Request::Destroy => {} _ => unreachable!(), } } + + fn destroyed( + handler: &mut D, + _client: wayland_backend::server::ClientId, + resource: &ZwlrVirtualPointerV1, + _data: &VirtualPointerUserData, + ) { + let pointer = VirtualPointer { + pointer: resource.clone(), + }; + + handler.destroy_virtual_pointer(pointer); + handler + .virtual_pointer_manager_state() + .virtual_pointers + .remove(resource); + } } #[macro_export] @@ -180,14 +550,14 @@ macro_rules! delegate_virtual_pointer { ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: $crate::protocols::virtual_pointer::VirtualPointerManagerGlobalData - ] => $crate::protocols::virtual_pointer::VirtualPointerManagerState); + ] => $crate::protocols::virtual_pointer::VirtualPointerManagerState); smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1: () ] => $crate::protocols::virtual_pointer::VirtualPointerManagerState); smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ - smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1: $crate::protocols::virtual_pointer::VirtualPointerState + smithay::reexports::wayland_protocols_wlr::virtual_pointer::v1::server::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1: $crate::protocols::virtual_pointer::VirtualPointerUserData ] => $crate::protocols::virtual_pointer::VirtualPointerManagerState); }; } From 8e32053c50705e5bd604cd9861b00836dc279ed2 Mon Sep 17 00:00:00 2001 From: sodiboo Date: Tue, 27 Aug 2024 19:43:32 +0200 Subject: [PATCH 4/7] deal with supressed key release edge-case; add allow-inhibiting property --- niri-config/src/lib.rs | 18 +++++++++++++++++- src/input/mod.rs | 41 +++++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index fb7ae2135..f60969763 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1113,6 +1113,7 @@ pub struct Bind { pub repeat: bool, pub cooldown: Option, pub allow_when_locked: bool, + pub allow_inhibiting: bool, } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] @@ -2863,6 +2864,7 @@ where let mut cooldown = None; let mut allow_when_locked = false; let mut allow_when_locked_node = None; + let mut allow_inhibiting = true; for (name, val) in &node.properties { match &***name { "repeat" => { @@ -2877,6 +2879,9 @@ where allow_when_locked = knuffel::traits::DecodeScalar::decode(val, ctx)?; allow_when_locked_node = Some(name); } + "allow-inhibiting" => { + allow_inhibiting = knuffel::traits::DecodeScalar::decode(val, ctx)?; + } name_str => { ctx.emit_error(DecodeError::unexpected( name, @@ -2898,6 +2903,7 @@ where repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }; if let Some(child) = children.next() { @@ -2926,6 +2932,7 @@ where repeat, cooldown, allow_when_locked, + allow_inhibiting, }) } Err(e) => { @@ -3314,7 +3321,7 @@ mod tests { Mod+Comma { consume-window-into-column; } Mod+1 { focus-workspace 1; } Mod+Shift+1 { focus-workspace "workspace-1"; } - Mod+Shift+E { quit skip-confirmation=true; } + Mod+Shift+E allow-inhibiting=false { quit skip-confirmation=true; } Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } } @@ -3625,6 +3632,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: true, + allow_inhibiting: true, }, Bind { key: Key { @@ -3635,6 +3643,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3645,6 +3654,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3655,6 +3665,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3665,6 +3676,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3675,6 +3687,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3687,6 +3700,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3697,6 +3711,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: false, }, Bind { key: Key { @@ -3707,6 +3722,7 @@ mod tests { repeat: true, cooldown: Some(Duration::from_millis(150)), allow_when_locked: false, + allow_inhibiting: true, }, ]), switch_events: SwitchBinds { diff --git a/src/input/mod.rs b/src/input/mod.rs index 58a711afa..64b4ae7c9 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -2830,29 +2830,30 @@ 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, }); } } } - if is_inhibiting_shortcuts - && !matches!( - final_bind, - Some(Bind { - action: Action::ChangeVt(_), - .. - }) - ) - { - return FilterResult::Forward; - } - 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) } @@ -2892,6 +2893,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, }); } @@ -3354,6 +3361,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }]); let comp_mod = CompositorMod::Super; @@ -3490,6 +3498,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3500,6 +3509,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3510,6 +3520,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3520,6 +3531,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3530,6 +3542,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, ]); From 3e819c84706633c003fb17c645f8b34a2e056360 Mon Sep 17 00:00:00 2001 From: sodiboo Date: Sat, 7 Sep 2024 21:26:00 +0200 Subject: [PATCH 5/7] add toggle-keyboard-shortcuts-inhibit bind --- niri-config/src/lib.rs | 31 +++++++++++++++++++++++++++++++ resources/default-config.kdl | 10 ++++++++++ src/input/mod.rs | 14 ++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index f60969763..efa555b18 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1196,6 +1196,7 @@ pub enum Action { ScreenshotWindow, #[knuffel(skip)] ScreenshotWindowById(u64), + ToggleKeyboardShortcutsInhibit, CloseWindow, #[knuffel(skip)] CloseWindowById(u64), @@ -2926,6 +2927,12 @@ where } } + // The toggle-inhibit action must always be uninhibitable. + // Otherwise, it would be impossible to trigger it. + if matches!(action, Action::ToggleKeyboardShortcutsInhibit) { + allow_inhibiting = false; + } + Ok(Self { key, action, @@ -3314,6 +3321,8 @@ mod tests { } binds { + Mod+Escape { toggle-keyboard-shortcuts-inhibit; } + Mod+Shift+Escape allow-inhibiting=true { toggle-keyboard-shortcuts-inhibit; } Mod+T allow-when-locked=true { spawn "alacritty"; } Mod+Q { close-window; } Mod+Shift+H { focus-monitor-left; } @@ -3623,6 +3632,28 @@ mod tests { }, ], binds: Binds(vec![ + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::Escape), + modifiers: Modifiers::COMPOSITOR, + }, + action: Action::ToggleKeyboardShortcutsInhibit, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: false, + }, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::Escape), + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + action: Action::ToggleKeyboardShortcutsInhibit, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: false, + }, Bind { key: Key { trigger: Trigger::Keysym(Keysym::t), diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 5696a88c9..964b477c4 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -499,6 +499,16 @@ binds { Ctrl+Print { screenshot-screen; } Alt+Print { screenshot-window; } + // Applications such as remote-desktop clients and software KVM switches may + // request that niri stops processing the keyboard shortcuts defined here + // so they may, for example, forward the key presses as-is to a remote machine. + // It's a good idea to bind an escape hatch to toggle the inhibitor, + // so a buggy application can't hold your session hostage. + // + // The allow-inhibiting=false property can be applied to other binds as well, + // which ensures niri always processes them, even when an inhibitor is active. + Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } + // The quit action will show a confirmation dialog to avoid accidental exits. Mod+Shift+E { quit; } Ctrl+Alt+Delete { quit; } diff --git a/src/input/mod.rs b/src/input/mod.rs index 64b4ae7c9..51f99c8a8 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -619,6 +619,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(); @@ -3064,6 +3077,7 @@ fn allowed_when_locked(action: &Action) -> bool { | Action::PowerOffMonitors | Action::PowerOnMonitors | Action::SwitchLayout(_) + | Action::ToggleKeyboardShortcutsInhibit ) } From 71f3b2c911eecdfe09b6f80fbaa2c8fd0e1cd303 Mon Sep 17 00:00:00 2001 From: sodiboo Date: Sat, 23 Nov 2024 20:10:31 +0100 Subject: [PATCH 6/7] add InputBackend extensions; use Device::output() for absolute pos events --- src/input/backend_ext.rs | 44 ++++++++++++++++++++++++++++++++ src/input/mod.rs | 39 ++++++++++++++++++++-------- src/protocols/virtual_pointer.rs | 8 +++--- 3 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 src/input/backend_ext.rs diff --git a/src/input/backend_ext.rs b/src/input/backend_ext.rs new file mode 100644 index 000000000..91a2aad45 --- /dev/null +++ b/src/input/backend_ext.rs @@ -0,0 +1,44 @@ +use ::input as libinput; +use smithay::backend::input; +use smithay::backend::winit::WinitVirtualDevice; +use smithay::output::Output; + +use crate::protocols::virtual_pointer::VirtualPointer; + +pub trait NiriInputBackend: input::InputBackend { + type NiriDevice: NiriInputDevice; +} +impl NiriInputBackend for T +where + Self::Device: NiriInputDevice, +{ + type NiriDevice = Self::Device; +} + +pub trait NiriInputDevice: input::Device { + // FIXME: should this be per-event? logically yes, + // but right now we only use it for virtual pointers, which have static outputs. + fn output(&self) -> Option; +} + +impl NiriInputDevice for libinput::Device { + fn output(&self) -> Option { + // FIXME: Allow specifying the output per-device? + // In that case, change the method to take a reference to our state or config or something + // (because we can't easily change the libinput Device struct) + None + } +} + +impl NiriInputDevice for WinitVirtualDevice { + fn output(&self) -> Option { + // here it's actually *correct* to return None, because there is only one output. + None + } +} + +impl NiriInputDevice for VirtualPointer { + fn output(&self) -> Option { + self.output().cloned() + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs index 51f99c8a8..eef85eca4 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,6 +28,7 @@ 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}; @@ -43,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; @@ -51,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)] @@ -265,8 +269,10 @@ impl State { where I::Device: 'static, { + let device_output = event.device().output(); + 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, @@ -1753,12 +1759,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(); @@ -2635,14 +2643,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>( + fn compute_absolute_location( &self, - evt: &E, + evt: &impl AbsolutePositionEvent, + fallback_output: Option<&Output>, ) -> Option> { - let output = self.niri.output_for_touch()?; + let output = evt.device().output(); + 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); @@ -2652,6 +2659,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( + &self, + evt: &impl AbsolutePositionEvent, + ) -> Option> { + self.compute_absolute_location(evt, self.niri.output_for_touch()) + } + fn on_touch_down(&mut self, evt: I::TouchDownEvent) { let Some(handle) = self.niri.seat.get_touch() else { return; diff --git a/src/protocols/virtual_pointer.rs b/src/protocols/virtual_pointer.rs index f55ddbb62..ff3cb3e98 100644 --- a/src/protocols/virtual_pointer.rs +++ b/src/protocols/virtual_pointer.rs @@ -7,8 +7,8 @@ use smithay::backend::input::{ 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_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_pointer; use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat; use smithay::reexports::wayland_server::{ @@ -41,7 +41,7 @@ pub struct VirtualPointer { #[derive(Debug)] pub struct VirtualPointerUserData { seat: Option, - output: Option, + output: Option, axis_frame: Mutex>, } @@ -55,7 +55,7 @@ impl VirtualPointer { self.data().seat.as_ref() } - pub fn output(&self) -> Option<&WlOutput> { + pub fn output(&self) -> Option<&Output> { self.data().output.as_ref() } @@ -355,7 +355,7 @@ where seat, output, id, - } => (id, seat, output), + } => (id, seat, output.as_ref().and_then(Output::from_resource)), zwlr_virtual_pointer_manager_v1::Request::Destroy => return, _ => unreachable!(), }; From 54400029bd25193c3b12ccd7563d8a4be3a92b67 Mon Sep 17 00:00:00 2001 From: sodiboo Date: Sat, 23 Nov 2024 21:55:47 +0100 Subject: [PATCH 7/7] implement WinitVirtualDevice::output() to the one single virtual output --- src/backend/winit.rs | 5 +++++ src/input/backend_ext.rs | 26 ++++++++++++++++---------- src/input/mod.rs | 4 ++-- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 3472ff3ef..ef74906c1 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -266,4 +266,9 @@ impl Winit { pub fn ipc_outputs(&self) -> Arc> { self.ipc_outputs.clone() } + + // FIXME: If/when the winit backend supports multiple outputs, then this method makes no sense. + pub fn single_output(&self) -> &Output { + &self.output + } } diff --git a/src/input/backend_ext.rs b/src/input/backend_ext.rs index 91a2aad45..e79c269a9 100644 --- a/src/input/backend_ext.rs +++ b/src/input/backend_ext.rs @@ -3,6 +3,8 @@ use smithay::backend::input; use smithay::backend::winit::WinitVirtualDevice; use smithay::output::Output; +use crate::backend::Backend; +use crate::niri::State; use crate::protocols::virtual_pointer::VirtualPointer; pub trait NiriInputBackend: input::InputBackend { @@ -16,29 +18,33 @@ where } pub trait NiriInputDevice: input::Device { - // FIXME: should this be per-event? logically yes, - // but right now we only use it for virtual pointers, which have static outputs. - fn output(&self) -> Option; + // 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; } impl NiriInputDevice for libinput::Device { - fn output(&self) -> Option { + fn output(&self, _state: &State) -> Option { // FIXME: Allow specifying the output per-device? - // In that case, change the method to take a reference to our state or config or something - // (because we can't easily change the libinput Device struct) None } } impl NiriInputDevice for WinitVirtualDevice { - fn output(&self) -> Option { - // here it's actually *correct* to return None, because there is only one output. - None + fn output(&self, state: &State) -> Option { + match state.backend { + Backend::Winit(ref winit) => Some(winit.single_output().clone()), + // returning None over panicking here because it's not worth panicking over + // and also, foreseeably, someone might want to, at some point, use `WinitInputBackend` + // for dirty hacks or mocking or whatever, in which case this will be useful. + _ => None, + } } } impl NiriInputDevice for VirtualPointer { - fn output(&self) -> Option { + fn output(&self, _: &State) -> Option { self.output().cloned() } } diff --git a/src/input/mod.rs b/src/input/mod.rs index eef85eca4..efe6706cf 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -269,7 +269,7 @@ impl State { where I::Device: 'static, { - let device_output = event.device().output(); + 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) = device_output.or_else(|| self.niri.output_for_tablet()) { @@ -2648,7 +2648,7 @@ impl State { evt: &impl AbsolutePositionEvent, fallback_output: Option<&Output>, ) -> Option> { - let output = evt.device().output(); + 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();