diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index fb7ae2135..efa555b18 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)] @@ -1195,6 +1196,7 @@ pub enum Action { ScreenshotWindow, #[knuffel(skip)] ScreenshotWindowById(u64), + ToggleKeyboardShortcutsInhibit, CloseWindow, #[knuffel(skip)] CloseWindowById(u64), @@ -2863,6 +2865,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 +2880,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 +2904,7 @@ where repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }; if let Some(child) = children.next() { @@ -2920,12 +2927,19 @@ 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, repeat, cooldown, allow_when_locked, + allow_inhibiting, }) } Err(e) => { @@ -3307,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; } @@ -3314,7 +3330,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; } } @@ -3616,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), @@ -3625,6 +3663,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: true, + allow_inhibiting: true, }, Bind { key: Key { @@ -3635,6 +3674,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3645,6 +3685,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3655,6 +3696,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3665,6 +3707,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3675,6 +3718,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3687,6 +3731,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3697,6 +3742,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: false, }, Bind { key: Key { @@ -3707,6 +3753,7 @@ mod tests { repeat: true, cooldown: Some(Duration::from_millis(150)), allow_when_locked: false, + allow_inhibiting: true, }, ]), switch_events: SwitchBinds { 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/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/handlers/mod.rs b/src/handlers/mod.rs index 9d147df4b..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, @@ -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::::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); + 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 000000000..e79c269a9 --- /dev/null +++ b/src/input/backend_ext.rs @@ -0,0 +1,50 @@ +use ::input as libinput; +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 { + type NiriDevice: NiriInputDevice; +} +impl 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; +} + +impl NiriInputDevice for libinput::Device { + fn output(&self, _state: &State) -> Option { + // FIXME: Allow specifying the output per-device? + None + } +} + +impl NiriInputDevice for WinitVirtualDevice { + 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, _: &State) -> Option { + self.output().cloned() + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs index ae2c2b056..efe6706cf 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(&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 { @@ -603,6 +625,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(); @@ -1724,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(); @@ -2606,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(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); @@ -2623,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; @@ -2773,6 +2819,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 @@ -2813,6 +2860,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, }); } } @@ -2820,10 +2871,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) } @@ -2863,6 +2923,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, }); } @@ -3028,6 +3094,7 @@ fn allowed_when_locked(action: &Action) -> bool { | Action::PowerOffMonitors | Action::PowerOnMonitors | Action::SwitchLayout(_) + | Action::ToggleKeyboardShortcutsInhibit ) } @@ -3325,6 +3392,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }]); let comp_mod = CompositorMod::Super; @@ -3349,6 +3417,7 @@ mod tests { mods, &screenshot_ui, disable_power_key_handling, + false, ) }; @@ -3365,6 +3434,7 @@ mod tests { mods, &screenshot_ui, disable_power_key_handling, + false, ) }; @@ -3459,6 +3529,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3469,6 +3540,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3479,6 +3551,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3489,6 +3562,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3499,6 +3573,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 88dbd0eeb..3bde9e6ef 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, 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, @@ -1814,11 +1821,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 +2022,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, @@ -2045,6 +2059,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 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..ff3cb3e98 --- /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, +} + +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<&Output> { + 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; + + 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: 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: HashSet::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>, + ) { + let (id, seat, output) = match request { + zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointer { seat, id } => { + (id, seat, None) + } + zwlr_virtual_pointer_manager_v1::Request::CreateVirtualPointerWithOutput { + seat, + output, + id, + } => (id, seat, output.as_ref().and_then(Output::from_resource)), + 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 +where + D: Dispatch, + D: VirtualPointerHandler, + D: 'static, +{ + fn request( + handler: &mut D, + _client: &Client, + resource: &ZwlrVirtualPointerV1, + request: ::Request, + _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 } => { + let event = VirtualPointerMotionEvent { + pointer, + time, + dx, + dy, + }; + handler.on_virtual_pointer_motion(event); + } + zwlr_virtual_pointer_v1::Request::MotionAbsolute { + time, + x, + y, + x_extent, + 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, + } => { + // 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 } => { + 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 => { + 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 } => { + 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 } => { + 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, + axis, + value, + discrete, + } => { + 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] +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::VirtualPointerUserData + ] => $crate::protocols::virtual_pointer::VirtualPointerManagerState); + }; +}