aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-04-25 09:49:40 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-04-25 02:00:18 -0700
commitb8a9be542fe6c6e99b1eac159188166f2bf2e82e (patch)
tree26c1c7c284bc8859e44e3efa9ac58ccf7ded83fd
parent59de6918b3c7f3bc8e988046b3fef2cead322f9a (diff)
downloadniri-b8a9be542fe6c6e99b1eac159188166f2bf2e82e.tar.gz
niri-b8a9be542fe6c6e99b1eac159188166f2bf2e82e.tar.bz2
niri-b8a9be542fe6c6e99b1eac159188166f2bf2e82e.zip
overview: Add touchscreen gestures
-rw-r--r--src/input/mod.rs41
-rw-r--r--src/input/touch_overview_grab.rs274
2 files changed, 312 insertions, 3 deletions
diff --git a/src/input/mod.rs b/src/input/mod.rs
index 44527b8a..8ab9bde1 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -35,6 +35,7 @@ use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerCons
use smithay::wayland::selection::data_device::DnDGrab;
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use touch_move_grab::TouchMoveGrab;
+use touch_overview_grab::TouchOverviewGrab;
use self::move_grab::MoveGrab;
use self::resize_grab::ResizeGrab;
@@ -56,6 +57,7 @@ pub mod scroll_tracker;
pub mod spatial_movement_grab;
pub mod swipe_tracker;
pub mod touch_move_grab;
+pub mod touch_overview_grab;
pub mod touch_resize_grab;
use backend_ext::{NiriInputBackend as InputBackend, NiriInputDevice as _};
@@ -3467,12 +3469,45 @@ impl State {
let mod_key = self.backend.mod_key(&self.niri.config.borrow());
if !handle.is_grabbed() {
- if let Some((window, _)) = under.window {
+ let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
+ let mods = modifiers_from_state(mods);
+ let mod_down = mods.contains(mod_key.to_modifiers());
+
+ if self.niri.layout.is_overview_open() && !mod_down && under.layer.is_none() {
+ let (output, pos_within_output) = self.niri.output_under(touch_location).unwrap();
+ let output = output.clone();
+
+ let mut matched_narrow = true;
+ let mut ws = self.niri.workspace_under(false, touch_location);
+ if ws.is_none() {
+ matched_narrow = false;
+ ws = self.niri.workspace_under(true, touch_location);
+ }
+ let ws_id = ws.map(|(_, ws)| ws.id());
+
+ let mapped = self.niri.window_under(touch_location);
+ let window = mapped.map(|mapped| mapped.window.clone());
+
+ let start_data = TouchGrabStartData {
+ focus: None,
+ slot: evt.slot(),
+ location: touch_location,
+ };
+ let start_timestamp = Duration::from_micros(evt.time());
+ let grab = TouchOverviewGrab::new(
+ start_data,
+ start_timestamp,
+ output,
+ pos_within_output,
+ ws_id,
+ matched_narrow,
+ window,
+ );
+ handle.set_grab(self, grab, serial);
+ } else if let Some((window, _)) = under.window {
self.niri.layout.activate_window(&window);
// Check if we need to start an interactive move.
- let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
- let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
if mod_down {
let (output, pos_within_output) =
self.niri.output_under(touch_location).unwrap();
diff --git a/src/input/touch_overview_grab.rs b/src/input/touch_overview_grab.rs
new file mode 100644
index 00000000..c5213ccc
--- /dev/null
+++ b/src/input/touch_overview_grab.rs
@@ -0,0 +1,274 @@
+use std::time::Duration;
+
+use smithay::desktop::Window;
+use smithay::input::touch::{
+ DownEvent, GrabStartData as TouchGrabStartData, MotionEvent, OrientationEvent, ShapeEvent,
+ TouchGrab, TouchInnerHandle, UpEvent,
+};
+use smithay::input::SeatHandler;
+use smithay::output::Output;
+use smithay::utils::{IsAlive, Logical, Point, Serial};
+
+use crate::layout::workspace::{Workspace, WorkspaceId};
+use crate::niri::State;
+use crate::window::Mapped;
+
+// When the touch is stationary for this much time, it becomes an interactive move.
+const INTERACTIVE_MOVE_THRESHOLD: Duration = Duration::from_millis(500);
+
+pub struct TouchOverviewGrab {
+ start_data: TouchGrabStartData<State>,
+ start_timestamp: Duration,
+ last_location: Point<f64, Logical>,
+ output: Output,
+ start_pos_within_output: Point<f64, Logical>,
+ workspace_id: Option<WorkspaceId>,
+ workspace_matched_narrow: bool,
+ window: Option<Window>,
+ gesture: GestureState,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum GestureState {
+ Recognizing,
+ ViewOffset,
+ WorkspaceSwitch,
+ InteractiveMove,
+}
+
+impl TouchOverviewGrab {
+ pub fn new(
+ start_data: TouchGrabStartData<State>,
+ start_timestamp: Duration,
+ output: Output,
+ start_pos_within_output: Point<f64, Logical>,
+ workspace_id: Option<WorkspaceId>,
+ workspace_matched_narrow: bool,
+ window: Option<Window>,
+ ) -> Self {
+ Self {
+ last_location: start_data.location,
+ start_timestamp,
+ start_data,
+ output,
+ start_pos_within_output,
+ workspace_id,
+ workspace_matched_narrow,
+ window,
+ gesture: GestureState::Recognizing,
+ }
+ }
+
+ fn on_ungrab(&mut self, state: &mut State) {
+ let layout = &mut state.niri.layout;
+ match self.gesture {
+ GestureState::Recognizing => {
+ // Tap to activate.
+ layout.focus_output(&self.output);
+
+ // Activate the workspace if necessary.
+ if self.window.is_some() || self.workspace_matched_narrow {
+ // When activating a window, we want to activate the window's current
+ // workspace. Otherwise, find the workspace that we tapped on.
+ let ws_matches = |ws: &Workspace<Mapped>| {
+ if let Some(window) = &self.window {
+ ws.has_window(window)
+ } else if let Some(ws_id) = self.workspace_id {
+ ws.id() == ws_id
+ } else {
+ false
+ }
+ };
+
+ let ws_idx = if let Some((Some(mon), ws_idx, _)) =
+ layout.workspaces().find(|(_, _, ws)| ws_matches(ws))
+ {
+ // The workspace could've moved to a different output in the meantime.
+ (*mon.output() == self.output).then_some(ws_idx)
+ } else {
+ None
+ };
+
+ if let Some(ws_idx) = ws_idx {
+ layout.toggle_overview_to_workspace(ws_idx);
+ }
+ }
+
+ if let Some(window) = self.window.as_ref() {
+ layout.activate_window(window);
+ }
+ }
+ GestureState::ViewOffset => {
+ layout.view_offset_gesture_end(Some(false));
+ }
+ GestureState::WorkspaceSwitch => {
+ layout.workspace_switch_gesture_end(Some(false));
+ }
+ GestureState::InteractiveMove => {
+ layout.interactive_move_end(self.window.as_ref().unwrap());
+ }
+ };
+
+ state.niri.queue_redraw_all();
+ }
+}
+
+impl TouchGrab<State> for TouchOverviewGrab {
+ fn down(
+ &mut self,
+ data: &mut State,
+ handle: &mut TouchInnerHandle<'_, State>,
+ _focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
+ event: &DownEvent,
+ seq: Serial,
+ ) {
+ handle.down(data, None, event, seq);
+ }
+
+ fn up(
+ &mut self,
+ data: &mut State,
+ handle: &mut TouchInnerHandle<'_, State>,
+ event: &UpEvent,
+ seq: Serial,
+ ) {
+ handle.up(data, event, seq);
+
+ if event.slot != self.start_data.slot {
+ return;
+ }
+
+ handle.unset_grab(self, data);
+ }
+
+ fn motion(
+ &mut self,
+ data: &mut State,
+ handle: &mut TouchInnerHandle<'_, State>,
+ _focus: Option<(<State as SeatHandler>::TouchFocus, Point<f64, Logical>)>,
+ event: &MotionEvent,
+ seq: Serial,
+ ) {
+ handle.motion(data, None, event, seq);
+
+ if event.slot != self.start_data.slot {
+ return;
+ }
+
+ let timestamp = Duration::from_millis(u64::from(event.time));
+ let layout = &mut data.niri.layout;
+
+ // Check if we should become interactive move.
+ if matches!(self.gesture, GestureState::Recognizing) {
+ if let Some(window) = self.window.as_ref().filter(|win| win.alive()) {
+ let passed = timestamp.saturating_sub(self.start_timestamp);
+ if INTERACTIVE_MOVE_THRESHOLD <= passed
+ && layout.interactive_move_begin(
+ window.clone(),
+ &self.output,
+ self.start_pos_within_output,
+ )
+ {
+ self.gesture = GestureState::InteractiveMove;
+ }
+ }
+ }
+
+ // Check if we should become a spatial scroll.
+ if matches!(self.gesture, GestureState::Recognizing) {
+ let c = event.location - self.start_data.location;
+
+ // Check if the gesture moved far enough to decide. Threshold copied from libadwaita.
+ if c.x * c.x + c.y * c.y >= 16. * 16. {
+ if let Some(ws_id) = self.workspace_id.filter(|_| c.x.abs() > c.y.abs()) {
+ if let Some((ws_idx, ws)) = layout.find_workspace_by_id(ws_id) {
+ if ws.current_output() == Some(&self.output) {
+ layout.view_offset_gesture_begin(&self.output, Some(ws_idx), false);
+ self.gesture = GestureState::ViewOffset;
+ }
+ }
+ }
+
+ if matches!(self.gesture, GestureState::Recognizing) {
+ layout.workspace_switch_gesture_begin(&self.output, false);
+ self.gesture = GestureState::WorkspaceSwitch;
+ }
+ }
+ }
+
+ // Do nothing if still recognizing.
+ if matches!(self.gesture, GestureState::Recognizing) {
+ return;
+ }
+
+ let delta = event.location - self.last_location;
+ self.last_location = event.location;
+
+ let ongoing = match self.gesture {
+ GestureState::Recognizing => unreachable!(),
+ GestureState::ViewOffset => layout
+ .view_offset_gesture_update(-delta.x, timestamp, false)
+ .is_some(),
+ GestureState::WorkspaceSwitch => layout
+ .workspace_switch_gesture_update(-delta.y, timestamp, false)
+ .is_some(),
+ GestureState::InteractiveMove => {
+ let window = self.window.as_ref().unwrap();
+ if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
+ let output = output.clone();
+ data.niri.layout.interactive_move_update(
+ window,
+ delta,
+ output,
+ pos_within_output,
+ )
+ } else {
+ false
+ }
+ }
+ };
+
+ if ongoing {
+ data.niri.queue_redraw_all();
+ } else {
+ handle.unset_grab(self, data);
+ }
+ }
+
+ fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
+ handle.frame(data, seq);
+ }
+
+ fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) {
+ handle.cancel(data, seq);
+ handle.unset_grab(self, data);
+ }
+
+ fn shape(
+ &mut self,
+ data: &mut State,
+ handle: &mut TouchInnerHandle<'_, State>,
+ event: &ShapeEvent,
+ seq: Serial,
+ ) {
+ handle.shape(data, event, seq);
+ }
+
+ fn orientation(
+ &mut self,
+ data: &mut State,
+ handle: &mut TouchInnerHandle<'_, State>,
+ event: &OrientationEvent,
+ seq: Serial,
+ ) {
+ handle.orientation(data, event, seq);
+ }
+
+ fn start_data(&self) -> &TouchGrabStartData<State> {
+ &self.start_data
+ }
+
+ fn unset(&mut self, data: &mut State) {
+ self.on_ungrab(data);
+ }
+}