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