aboutsummaryrefslogtreecommitdiff
path: root/src/input/mod.rs
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-05-11 13:21:05 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-05-11 13:21:05 +0400
commitbeba87354a1fd30a95eaaf6c98eec72797e4baa7 (patch)
tree0298d7911dd5daee544b39ae7ee1f537e119cadf /src/input/mod.rs
parent078724369d464d5184a3d93a1b71c10092092d0a (diff)
downloadniri-beba87354a1fd30a95eaaf6c98eec72797e4baa7.tar.gz
niri-beba87354a1fd30a95eaaf6c98eec72797e4baa7.tar.bz2
niri-beba87354a1fd30a95eaaf6c98eec72797e4baa7.zip
Group input-related things in a subfolder
Diffstat (limited to 'src/input/mod.rs')
-rw-r--r--src/input/mod.rs2565
1 files changed, 2565 insertions, 0 deletions
diff --git a/src/input/mod.rs b/src/input/mod.rs
new file mode 100644
index 00000000..69650a12
--- /dev/null
+++ b/src/input/mod.rs
@@ -0,0 +1,2565 @@
+use std::any::Any;
+use std::collections::hash_map::Entry;
+use std::collections::HashSet;
+use std::time::Duration;
+
+use calloop::timer::{TimeoutAction, Timer};
+use input::event::gesture::GestureEventCoordinates as _;
+use niri_config::{Action, Bind, Binds, Key, Modifiers, Trigger};
+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, MouseButton, PointerAxisEvent,
+ PointerButtonEvent, PointerMotionEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent,
+ TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent,
+};
+use smithay::backend::libinput::LibinputInputBackend;
+use smithay::input::keyboard::{keysyms, FilterResult, Keysym, ModifiersState};
+use smithay::input::pointer::{
+ AxisFrame, ButtonEvent, CursorImageStatus, Focus, GestureHoldBeginEvent, GestureHoldEndEvent,
+ GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
+ GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
+ MotionEvent, RelativeMotionEvent,
+};
+use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent};
+use smithay::utils::{Logical, Point, SERIAL_COUNTER};
+use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
+use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
+
+use self::resize_grab::ResizeGrab;
+use crate::niri::State;
+use crate::ui::screenshot_ui::ScreenshotUi;
+use crate::utils::spawning::spawn;
+use crate::utils::{center, get_monotonic_time, ResizeEdge};
+
+pub mod resize_grab;
+pub mod scroll_tracker;
+pub mod swipe_tracker;
+
+pub const DOUBLE_CLICK_TIME: Duration = Duration::from_millis(400);
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum CompositorMod {
+ Super,
+ Alt,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct TabletData {
+ pub aspect_ratio: f64,
+}
+
+impl State {
+ pub fn process_input_event<I: InputBackend + 'static>(&mut self, event: InputEvent<I>)
+ where
+ I::Device: 'static, // Needed for downcasting.
+ {
+ let _span = tracy_client::span!("process_input_event");
+
+ // A bit of a hack, but animation end runs some logic (i.e. workspace clean-up) and it
+ // doesn't always trigger due to damage, etc. So run it here right before it might prove
+ // important. Besides, animations affect the input, so it's best to have up-to-date values
+ // here.
+ self.niri.layout.advance_animations(get_monotonic_time());
+
+ if self.niri.monitors_active {
+ // Notify the idle-notifier of activity.
+ if should_notify_activity(&event) {
+ let _span = tracy_client::span!("IdleNotifierState::notify_activity");
+ self.niri
+ .idle_notifier_state
+ .notify_activity(&self.niri.seat);
+ }
+ } else {
+ // Power on monitors if they were off.
+ if should_activate_monitors(&event) {
+ self.niri.activate_monitors(&mut self.backend);
+
+ // Notify the idle-notifier of activity only if we're also powering on the
+ // monitors.
+ self.niri
+ .idle_notifier_state
+ .notify_activity(&self.niri.seat);
+ }
+ }
+
+ let hide_hotkey_overlay =
+ self.niri.hotkey_overlay.is_open() && should_hide_hotkey_overlay(&event);
+
+ let hide_exit_confirm_dialog = self
+ .niri
+ .exit_confirm_dialog
+ .as_ref()
+ .map_or(false, |d| d.is_open())
+ && should_hide_exit_confirm_dialog(&event);
+
+ use InputEvent::*;
+ match event {
+ DeviceAdded { device } => self.on_device_added(device),
+ DeviceRemoved { device } => self.on_device_removed(device),
+ Keyboard { event } => self.on_keyboard::<I>(event),
+ PointerMotion { event } => self.on_pointer_motion::<I>(event),
+ PointerMotionAbsolute { event } => self.on_pointer_motion_absolute::<I>(event),
+ PointerButton { event } => self.on_pointer_button::<I>(event),
+ PointerAxis { event } => self.on_pointer_axis::<I>(event),
+ TabletToolAxis { event } => self.on_tablet_tool_axis::<I>(event),
+ TabletToolTip { event } => self.on_tablet_tool_tip::<I>(event),
+ TabletToolProximity { event } => self.on_tablet_tool_proximity::<I>(event),
+ TabletToolButton { event } => self.on_tablet_tool_button::<I>(event),
+ GestureSwipeBegin { event } => self.on_gesture_swipe_begin::<I>(event),
+ GestureSwipeUpdate { event } => self.on_gesture_swipe_update::<I>(event),
+ GestureSwipeEnd { event } => self.on_gesture_swipe_end::<I>(event),
+ GesturePinchBegin { event } => self.on_gesture_pinch_begin::<I>(event),
+ GesturePinchUpdate { event } => self.on_gesture_pinch_update::<I>(event),
+ GesturePinchEnd { event } => self.on_gesture_pinch_end::<I>(event),
+ GestureHoldBegin { event } => self.on_gesture_hold_begin::<I>(event),
+ GestureHoldEnd { event } => self.on_gesture_hold_end::<I>(event),
+ TouchDown { event } => self.on_touch_down::<I>(event),
+ TouchMotion { event } => self.on_touch_motion::<I>(event),
+ TouchUp { event } => self.on_touch_up::<I>(event),
+ TouchCancel { event } => self.on_touch_cancel::<I>(event),
+ TouchFrame { event } => self.on_touch_frame::<I>(event),
+ SwitchToggle { .. } => (),
+ Special(_) => (),
+ }
+
+ // Do this last so that screenshot still gets it.
+ // FIXME: do this in a less cursed fashion somehow.
+ if hide_hotkey_overlay && self.niri.hotkey_overlay.hide() {
+ self.niri.queue_redraw_all();
+ }
+
+ if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
+ if hide_exit_confirm_dialog && dialog.hide() {
+ self.niri.queue_redraw_all();
+ }
+ }
+ }
+
+ pub fn process_libinput_event(&mut self, event: &mut InputEvent<LibinputInputBackend>) {
+ let _span = tracy_client::span!("process_libinput_event");
+
+ match event {
+ InputEvent::DeviceAdded { device } => {
+ self.niri.devices.insert(device.clone());
+
+ if device.has_capability(input::DeviceCapability::TabletTool) {
+ match device.size() {
+ Some((w, h)) => {
+ let aspect_ratio = w / h;
+ let data = TabletData { aspect_ratio };
+ self.niri.tablets.insert(device.clone(), data);
+ }
+ None => {
+ warn!("tablet tool device has no size");
+ }
+ }
+ }
+
+ if device.has_capability(input::DeviceCapability::Keyboard) {
+ if let Some(led_state) = self
+ .niri
+ .seat
+ .get_keyboard()
+ .map(|keyboard| keyboard.led_state())
+ {
+ device.led_update(led_state.into());
+ }
+ }
+
+ if device.has_capability(input::DeviceCapability::Touch) {
+ self.niri.touch.insert(device.clone());
+ }
+
+ apply_libinput_settings(&self.niri.config.borrow().input, device);
+ }
+ InputEvent::DeviceRemoved { device } => {
+ self.niri.touch.remove(device);
+ self.niri.tablets.remove(device);
+ self.niri.devices.remove(device);
+ }
+ _ => (),
+ }
+ }
+
+ fn on_device_added(&mut self, device: impl Device) {
+ if device.has_capability(DeviceCapability::TabletTool) {
+ let tablet_seat = self.niri.seat.tablet_seat();
+
+ let desc = TabletDescriptor::from(&device);
+ tablet_seat.add_tablet::<Self>(&self.niri.display_handle, &desc);
+ }
+ if device.has_capability(DeviceCapability::Touch) && self.niri.seat.get_touch().is_none() {
+ self.niri.seat.add_touch();
+ }
+ }
+
+ fn on_device_removed(&mut self, device: impl Device) {
+ if device.has_capability(DeviceCapability::TabletTool) {
+ let tablet_seat = self.niri.seat.tablet_seat();
+
+ let desc = TabletDescriptor::from(&device);
+ tablet_seat.remove_tablet(&desc);
+
+ // If there are no tablets in seat we can remove all tools
+ if tablet_seat.count_tablets() == 0 {
+ tablet_seat.clear_tools();
+ }
+ }
+ if device.has_capability(DeviceCapability::Touch) && self.niri.touch.is_empty() {
+ self.niri.seat.remove_touch();
+ }
+ }
+
+ /// Computes the cursor position for the tablet event.
+ ///
+ /// This function handles the tablet output mapping, as well as coordinate clamping and aspect
+ /// ratio correction.
+ fn compute_tablet_position<I: InputBackend>(
+ &self,
+ event: &(impl Event<I> + TabletToolEvent<I>),
+ ) -> Option<Point<f64, Logical>>
+ where
+ I::Device: 'static,
+ {
+ let output = self.niri.output_for_tablet()?;
+ let output_geo = self.niri.global_space.output_geometry(output).unwrap();
+
+ let mut pos = event.position_transformed(output_geo.size);
+ pos.x /= output_geo.size.w as f64;
+ pos.y /= output_geo.size.h as f64;
+
+ let device = event.device();
+ if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
+ if let Some(data) = self.niri.tablets.get(device) {
+ // This code does the same thing as mutter with "keep aspect ratio" enabled.
+ let output_aspect_ratio = output_geo.size.w as f64 / output_geo.size.h as f64;
+ let ratio = data.aspect_ratio / output_aspect_ratio;
+
+ if ratio > 1. {
+ pos.x *= ratio;
+ } else {
+ pos.y /= ratio;
+ }
+ }
+ };
+
+ pos.x *= output_geo.size.w as f64;
+ pos.y *= output_geo.size.h as f64;
+ pos.x = pos.x.clamp(0.0, output_geo.size.w as f64 - 1.);
+ pos.y = pos.y.clamp(0.0, output_geo.size.h as f64 - 1.);
+ Some(pos + output_geo.loc.to_f64())
+ }
+
+ fn on_keyboard<I: InputBackend>(&mut self, event: I::KeyboardKeyEvent) {
+ let comp_mod = self.backend.mod_key();
+
+ let serial = SERIAL_COUNTER.next_serial();
+ let time = Event::time_msec(&event);
+ let pressed = event.state() == KeyState::Pressed;
+
+ let Some(Some(bind)) = self.niri.seat.get_keyboard().unwrap().input(
+ self,
+ event.key_code(),
+ event.state(),
+ serial,
+ time,
+ |this, mods, keysym| {
+ let bindings = &this.niri.config.borrow().binds;
+ let key_code = event.key_code();
+ let modified = keysym.modified_sym();
+ let raw = keysym.raw_latin_sym_or_raw_current_sym();
+
+ if let Some(dialog) = &this.niri.exit_confirm_dialog {
+ if dialog.is_open() && pressed && raw == Some(Keysym::Return) {
+ info!("quitting after confirming exit dialog");
+ this.niri.stop_signal.stop();
+ }
+ }
+
+ should_intercept_key(
+ &mut this.niri.suppressed_keys,
+ bindings,
+ comp_mod,
+ key_code,
+ modified,
+ raw,
+ pressed,
+ *mods,
+ &this.niri.screenshot_ui,
+ this.niri.config.borrow().input.disable_power_key_handling,
+ )
+ },
+ ) else {
+ return;
+ };
+
+ // Filter actions when the key is released or the session is locked.
+ if !pressed {
+ return;
+ }
+
+ self.handle_bind(bind);
+ }
+
+ pub fn handle_bind(&mut self, bind: Bind) {
+ let Some(cooldown) = bind.cooldown else {
+ self.do_action(bind.action, bind.allow_when_locked);
+ return;
+ };
+
+ // Check this first so that it doesn't trigger the cooldown.
+ if self.niri.is_locked() && !(bind.allow_when_locked || allowed_when_locked(&bind.action)) {
+ return;
+ }
+
+ match self.niri.bind_cooldown_timers.entry(bind.key) {
+ // The bind is on cooldown.
+ Entry::Occupied(_) => (),
+ Entry::Vacant(entry) => {
+ let timer = Timer::from_duration(cooldown);
+ let token = self
+ .niri
+ .event_loop
+ .insert_source(timer, move |_, _, state| {
+ if state.niri.bind_cooldown_timers.remove(&bind.key).is_none() {
+ error!("bind cooldown timer entry disappeared");
+ }
+ TimeoutAction::Drop
+ })
+ .unwrap();
+ entry.insert(token);
+
+ self.do_action(bind.action, bind.allow_when_locked);
+ }
+ }
+ }
+
+ pub fn do_action(&mut self, action: Action, allow_when_locked: bool) {
+ if self.niri.is_locked() && !(allow_when_locked || allowed_when_locked(&action)) {
+ return;
+ }
+
+ if let Some(touch) = self.niri.seat.get_touch() {
+ touch.cancel(self);
+ }
+
+ match action {
+ Action::Quit(skip_confirmation) => {
+ if !skip_confirmation {
+ if let Some(dialog) = &mut self.niri.exit_confirm_dialog {
+ if dialog.show() {
+ self.niri.queue_redraw_all();
+ }
+ return;
+ }
+ }
+
+ info!("quitting as requested");
+ self.niri.stop_signal.stop()
+ }
+ Action::ChangeVt(vt) => {
+ self.backend.change_vt(vt);
+ // Changing VT may not deliver the key releases, so clear the state.
+ self.niri.suppressed_keys.clear();
+ }
+ Action::Suspend => {
+ self.backend.suspend();
+ // Suspend may not deliver the key releases, so clear the state.
+ self.niri.suppressed_keys.clear();
+ }
+ Action::PowerOffMonitors => {
+ self.niri.deactivate_monitors(&mut self.backend);
+ }
+ Action::ToggleDebugTint => {
+ self.backend.toggle_debug_tint();
+ self.niri.queue_redraw_all();
+ }
+ Action::DebugToggleOpaqueRegions => {
+ self.niri.debug_draw_opaque_regions = !self.niri.debug_draw_opaque_regions;
+ self.niri.queue_redraw_all();
+ }
+ Action::DebugToggleDamage => {
+ self.niri.debug_toggle_damage();
+ }
+ Action::Spawn(command) => {
+ spawn(command);
+ }
+ Action::DoScreenTransition(delay_ms) => {
+ self.backend.with_primary_renderer(|renderer| {
+ self.niri.do_screen_transition(renderer, delay_ms);
+ });
+ }
+ Action::ScreenshotScreen => {
+ let active = self.niri.layout.active_output().cloned();
+ if let Some(active) = active {
+ self.backend.with_primary_renderer(|renderer| {
+ if let Err(err) = self.niri.screenshot(renderer, &active) {
+ warn!("error taking screenshot: {err:?}");
+ }
+ });
+ }
+ }
+ Action::ConfirmScreenshot => {
+ self.backend.with_primary_renderer(|renderer| {
+ match self.niri.screenshot_ui.capture(renderer) {
+ Ok((size, pixels)) => {
+ if let Err(err) = self.niri.save_screenshot(size, pixels) {
+ warn!("error saving screenshot: {err:?}");
+ }
+ }
+ Err(err) => {
+ warn!("error capturing screenshot: {err:?}");
+ }
+ }
+ });
+
+ self.niri.screenshot_ui.close();
+ self.niri
+ .cursor_manager
+ .set_cursor_image(CursorImageStatus::default_named());
+ self.niri.queue_redraw_all();
+ }
+ Action::CancelScreenshot => {
+ self.niri.screenshot_ui.close();
+ self.niri
+ .cursor_manager
+ .set_cursor_image(CursorImageStatus::default_named());
+ self.niri.queue_redraw_all();
+ }
+ Action::Screenshot => {
+ self.backend.with_primary_renderer(|renderer| {
+ self.niri.open_screenshot_ui(renderer);
+ });
+ }
+ Action::ScreenshotWindow => {
+ let active = self.niri.layout.active_window();
+ if let Some((mapped, output)) = active {
+ self.backend.with_primary_renderer(|renderer| {
+ if let Err(err) = self.niri.screenshot_window(renderer, output, mapped) {
+ warn!("error taking screenshot: {err:?}");
+ }
+ });
+ }
+ }
+ Action::CloseWindow => {
+ if let Some(mapped) = self.niri.layout.focus() {
+ mapped.toplevel().send_close();
+ }
+ }
+ Action::FullscreenWindow => {
+ let focus = self.niri.layout.focus().map(|m| m.window.clone());
+ if let Some(window) = focus {
+ self.niri.layout.toggle_fullscreen(&window);
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ }
+ Action::SwitchLayout(action) => {
+ self.niri.seat.get_keyboard().unwrap().with_xkb_state(
+ self,
+ |mut state| match action {
+ LayoutSwitchTarget::Next => state.cycle_next_layout(),
+ LayoutSwitchTarget::Prev => state.cycle_prev_layout(),
+ },
+ );
+ }
+ Action::MoveColumnLeft => {
+ self.niri.layout.move_left();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveColumnRight => {
+ self.niri.layout.move_right();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveColumnToFirst => {
+ self.niri.layout.move_column_to_first();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveColumnToLast => {
+ self.niri.layout.move_column_to_last();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWindowDown => {
+ self.niri.layout.move_down();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWindowUp => {
+ self.niri.layout.move_up();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWindowDownOrToWorkspaceDown => {
+ self.niri.layout.move_down_or_to_workspace_down();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWindowUpOrToWorkspaceUp => {
+ self.niri.layout.move_up_or_to_workspace_up();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::ConsumeOrExpelWindowLeft => {
+ self.niri.layout.consume_or_expel_window_left();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::ConsumeOrExpelWindowRight => {
+ self.niri.layout.consume_or_expel_window_right();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusColumnLeft => {
+ self.niri.layout.focus_left();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusColumnRight => {
+ self.niri.layout.focus_right();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusColumnFirst => {
+ self.niri.layout.focus_column_first();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusColumnLast => {
+ self.niri.layout.focus_column_last();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusWindowDown => {
+ self.niri.layout.focus_down();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusWindowUp => {
+ self.niri.layout.focus_up();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusWindowOrWorkspaceDown => {
+ self.niri.layout.focus_window_or_workspace_down();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusWindowOrWorkspaceUp => {
+ self.niri.layout.focus_window_or_workspace_up();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWindowToWorkspaceDown => {
+ self.niri.layout.move_to_workspace_down();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWindowToWorkspaceUp => {
+ self.niri.layout.move_to_workspace_up();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWindowToWorkspace(idx) => {
+ let idx = idx.saturating_sub(1) as usize;
+ self.niri.layout.move_to_workspace(idx);
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveColumnToWorkspaceDown => {
+ self.niri.layout.move_column_to_workspace_down();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveColumnToWorkspaceUp => {
+ self.niri.layout.move_column_to_workspace_up();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveColumnToWorkspace(idx) => {
+ let idx = idx.saturating_sub(1) as usize;
+ self.niri.layout.move_column_to_workspace(idx);
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusWorkspaceDown => {
+ self.niri.layout.switch_workspace_down();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusWorkspaceUp => {
+ self.niri.layout.switch_workspace_up();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusWorkspace(idx) => {
+ let idx = idx.saturating_sub(1) as usize;
+
+ let config = &self.niri.config;
+ if config.borrow().input.workspace_auto_back_and_forth {
+ self.niri.layout.switch_workspace_auto_back_and_forth(idx);
+ } else {
+ self.niri.layout.switch_workspace(idx);
+ }
+
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::FocusWorkspacePrevious => {
+ self.niri.layout.switch_workspace_previous();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWorkspaceDown => {
+ self.niri.layout.move_workspace_down();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MoveWorkspaceUp => {
+ self.niri.layout.move_workspace_up();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::ConsumeWindowIntoColumn => {
+ self.niri.layout.consume_into_column();
+ // This does not cause immediate focus or window size change, so warping mouse to
+ // focus won't do anything here.
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::ExpelWindowFromColumn => {
+ self.niri.layout.expel_from_column();
+ self.maybe_warp_cursor_to_focus();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::SwitchPresetColumnWidth => {
+ self.niri.layout.toggle_width();
+ }
+ Action::CenterColumn => {
+ self.niri.layout.center_column();
+ // FIXME: granular
+ self.niri.queue_redraw_all();
+ }
+ Action::MaximizeColumn => {
+ self.niri.layout.toggle_full_width();
+ }
+ Action::FocusMonitorLeft => {
+ if let Some(output) = self.niri.output_left() {
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::FocusMonitorRight => {
+ if let Some(output) = self.niri.output_right() {
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::FocusMonitorDown => {
+ if let Some(output) = self.niri.output_down() {
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::FocusMonitorUp => {
+ if let Some(output) = self.niri.output_up() {
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveWindowToMonitorLeft => {
+ if let Some(output) = self.niri.output_left() {
+ self.niri.layout.move_to_output(&output);
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveWindowToMonitorRight => {
+ if let Some(output) = self.niri.output_right() {
+ self.niri.layout.move_to_output(&output);
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveWindowToMonitorDown => {
+ if let Some(output) = self.niri.output_down() {
+ self.niri.layout.move_to_output(&output);
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveWindowToMonitorUp => {
+ if let Some(output) = self.niri.output_up() {
+ self.niri.layout.move_to_output(&output);
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveColumnToMonitorLeft => {
+ if let Some(output) = self.niri.output_left() {
+ self.niri.layout.move_column_to_output(&output);
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveColumnToMonitorRight => {
+ if let Some(output) = self.niri.output_right() {
+ self.niri.layout.move_column_to_output(&output);
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveColumnToMonitorDown => {
+ if let Some(output) = self.niri.output_down() {
+ self.niri.layout.move_column_to_output(&output);
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveColumnToMonitorUp => {
+ if let Some(output) = self.niri.output_up() {
+ self.niri.layout.move_column_to_output(&output);
+ self.niri.layout.focus_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::SetColumnWidth(change) => {
+ self.niri.layout.set_column_width(change);
+ }
+ Action::SetWindowHeight(change) => {
+ self.niri.layout.set_window_height(change);
+ }
+ Action::ResetWindowHeight => {
+ self.niri.layout.reset_window_height();
+ }
+ Action::ShowHotkeyOverlay => {
+ if self.niri.hotkey_overlay.show() {
+ self.niri.queue_redraw_all();
+ }
+ }
+ Action::MoveWorkspaceToMonitorLeft => {
+ if let Some(output) = self.niri.output_left() {
+ self.niri.layout.move_workspace_to_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveWorkspaceToMonitorRight => {
+ if let Some(output) = self.niri.output_right() {
+ self.niri.layout.move_workspace_to_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveWorkspaceToMonitorDown => {
+ if let Some(output) = self.niri.output_down() {
+ self.niri.layout.move_workspace_to_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ Action::MoveWorkspaceToMonitorUp => {
+ if let Some(output) = self.niri.output_up() {
+ self.niri.layout.move_workspace_to_output(&output);
+ if !self.maybe_warp_cursor_to_focus_centered() {
+ self.move_cursor_to_output(&output);
+ }
+ }
+ }
+ }
+ }
+
+ fn on_pointer_motion<I: InputBackend>(&mut self, event: I::PointerMotionEvent) {
+ // We need an output to be able to move the pointer.
+ if self.niri.global_space.outputs().next().is_none() {
+ return;
+ }
+
+ let serial = SERIAL_COUNTER.next_serial();
+
+ let pointer = self.niri.seat.get_pointer().unwrap();
+
+ let pos = pointer.current_location();
+
+ // We have an output, so we can compute the new location and focus.
+ let mut new_pos = pos + event.delta();
+
+ // We received an event for the regular pointer, so show it now.
+ self.niri.pointer_hidden = false;
+ self.niri.tablet_cursor_location = None;
+
+ // Check if we have an active pointer constraint.
+ let mut pointer_confined = None;
+ if let Some(focus) = &self.niri.pointer_focus.surface {
+ let pos_within_surface = pos.to_i32_round() - focus.1;
+
+ let mut pointer_locked = false;
+ with_pointer_constraint(&focus.0, &pointer, |constraint| {
+ let Some(constraint) = constraint else { return };
+ if !constraint.is_active() {
+ return;
+ }
+
+ // Constraint does not apply if not within region.
+ if let Some(region) = constraint.region() {
+ if !region.contains(pos_within_surface) {
+ return;
+ }
+ }
+
+ match &*constraint {
+ PointerConstraint::Locked(_locked) => {
+ pointer_locked = true;
+ }
+ PointerConstraint::Confined(confine) => {
+ pointer_confined = Some((focus.clone(), confine.region().cloned()));
+ }
+ }
+ });
+
+ // If the pointer is locked, only send relative motion.
+ if pointer_locked {
+ pointer.relative_motion(
+ self,
+ Some(focus.clone()),
+ &RelativeMotionEvent {
+ delta: event.delta(),
+ delta_unaccel: event.delta_unaccel(),
+ utime: event.time(),
+ },