diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2025-04-25 09:49:40 +0300 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2025-04-25 02:00:18 -0700 |
| commit | b8a9be542fe6c6e99b1eac159188166f2bf2e82e (patch) | |
| tree | 26c1c7c284bc8859e44e3efa9ac58ccf7ded83fd /src | |
| parent | 59de6918b3c7f3bc8e988046b3fef2cead322f9a (diff) | |
| download | niri-b8a9be542fe6c6e99b1eac159188166f2bf2e82e.tar.gz niri-b8a9be542fe6c6e99b1eac159188166f2bf2e82e.tar.bz2 niri-b8a9be542fe6c6e99b1eac159188166f2bf2e82e.zip | |
overview: Add touchscreen gestures
Diffstat (limited to 'src')
| -rw-r--r-- | src/input/mod.rs | 41 | ||||
| -rw-r--r-- | src/input/touch_overview_grab.rs | 274 |
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); + } +} |
