// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Wayland touch → ltk dispatch. //! //! `wl_touch` down / up / motion map directly onto the three //! lifecycle methods of [`GestureState`](super::gesture::GestureState), //! same as pointer press / release / motion. Touch has no hover and //! no scroll-axis event, so this file is shorter than `pointer.rs`; //! the two handlers share `apply_move_outcome` / //! `apply_release_events` in `dispatch.rs`. //! //! The first finger to land on a surface becomes its **primary slot** //! and drives the single-slot gesture machine (swipe, scroll, //! long-press, drag). Any additional finger arriving while the //! primary is held bypasses the gesture machine and surfaces directly //! through [`App::on_touch_down`] / [`App::on_touch_move`] / //! [`App::on_touch_up`], so apps can implement pinch-zoom or //! two-finger pan without losing the built-in gestures. Slot //! ownership is recorded on `SurfaceState::primary_touch_id` plus //! `SurfaceState::touch_slots` for the auxiliary fingers' last //! position (Wayland `wl_touch.up` does not carry one, so the slot //! cache supplies it). //! //! The cross-surface routing map `AppData::touch_focus` is still //! per touch id, so an auxiliary slot that landed on an overlay //! keeps reporting through that overlay's `App` callback even if //! the finger drifts over the main surface. use smithay_client_toolkit::seat::touch::TouchHandler; use smithay_client_toolkit::reexports::client:: { protocol::{ wl_surface::WlSurface, wl_touch::WlTouch }, Connection, QueueHandle, }; use crate::app::App; use crate::event_loop::{ AppData, SurfaceFocus, SurfaceState }; use crate::tree::find_handlers; impl TouchHandler for AppData { fn down( &mut self, _conn: &Connection, qh: &QueueHandle, _touch: &WlTouch, serial: u32, _time: u32, surface: WlSurface, id: i32, position: ( f64, f64 ), ) { self.last_input_serial = serial; let focus = self.focus_for_surface( &surface ).unwrap_or( SurfaceFocus::Main ); self.touch_focus.insert( id, focus ); let pos = self.surface( focus ).to_physical( position.0, position.1 ); self.pointer_pos = pos; // Compositor re-grab after the origin surface died mid-drag: this // slot is already our primary and the drag state was migrated here, // so treat the redundant `down` as a continuation, not a restart. if self.surface( focus ).primary_touch_id == Some( id ) { return; } // Auxiliary slot path: a second (or third…) finger arriving // while another finger already drives the primary slot stays // out of the single-slot gesture machine entirely. The app // gets a raw `on_touch_down` and can drive its own pinch / // pan from the per-id stream. let is_primary = { let ss = self.surface_mut( focus ); if ss.primary_touch_id.is_none() { ss.primary_touch_id = Some( id ); true } else { ss.touch_slots.insert( id, pos ); false } }; if !is_primary { self.app.on_touch_down( id as i64, pos.x, pos.y ); return; } if matches!( focus, SurfaceFocus::Main ) && !self.overlays.is_empty() { self.dismiss_main_outside_popups( pos ); } // Built-in context menu intercepts the touch before the // regular gesture machine — same logic as the pointer path. if self.surface( focus ).context_menu.is_some() { if self.handle_context_menu_press( focus, pos ) { return; } } let outcome = { let ss = self.surface_mut( focus ); let result = ss.gesture.on_press( pos, &ss.frame.widget_rects, &ss.frame.scroll_rects ); ss.needs_redraw = true; result }; self.set_focus( focus, outcome.hit_idx, qh ); if let Some( msg ) = outcome.initial_slider_msg { self.pending_msgs.push( msg ); } // Press-and-hold repeat — same wiring as the pointer path. if let Some( idx ) = outcome.hit_idx { let immediate = { let handlers = find_handlers( &self.surface( focus ).frame.widget_rects, idx ); if matches!( handlers, Some( crate::widget::WidgetHandlers::Button { repeating: true, .. } ) ) { handlers.and_then( |h| h.press_msg() ) } else { None } }; if let Some( msg ) = immediate { self.pending_msgs.push( msg ); self.start_button_repeat( focus, idx ); } } // Click-to-position the text cursor for touch presses too, // and double-tap selects the word under the press. if let Some( idx ) = outcome.hit_idx { let is_text = find_handlers( &self.surface( focus ).frame.widget_rects, idx ) .map( |h| h.is_text_input() ).unwrap_or( false ); if is_text { // Eye icon hit on a password field short-circuits // the text-edit dispatch — fire the toggle msg and // skip cursor placement. if self.handle_password_toggle_press( focus, idx, pos ) { let _ = self.note_press_for_double_click( pos ); } else { let is_double = self.note_press_for_double_click( pos ); if is_double { self.handle_text_select_word( focus, idx, pos ); } else { self.handle_text_pointer_down( focus, idx, pos ); } } } else { let _ = self.note_press_for_double_click( pos ); } } else { let _ = self.note_press_for_double_click( pos ); } } fn up( &mut self, _conn: &Connection, _qh: &QueueHandle, _touch: &WlTouch, _serial: u32, _time: u32, id: i32, ) { let focus = self.touch_focus.remove( &id ).unwrap_or( SurfaceFocus::Main ); // Auxiliary release: not the primary slot → recover the last // recorded position for the up callback (Wayland's `wl_touch.up` // does not carry one) and bail before reaching the gesture // machine. let is_primary = { let ss = self.surface_mut( focus ); ss.primary_touch_id == Some( id ) }; if !is_primary { let pos = { let ss = self.surface_mut( focus ); ss.touch_slots.remove( &id ).unwrap_or( self.pointer_pos ) }; self.app.on_touch_up( id as i64, pos.x, pos.y ); return; } // Touch-up does not carry a position in wl_touch — the last // motion's position is the release point. let pos = self.pointer_pos; let global_drag = self.has_active_long_press_drag(); let swipe = self.swipe_config( focus ); let events_out = { let ss = self.surface_mut( focus ); ss.needs_redraw = true; ss.primary_touch_id = None; ss.gesture.on_release( pos, &ss.frame.widget_rects, &swipe, global_drag ) }; self.apply_release_events( focus, events_out ); self.stop_button_repeat(); } fn motion( &mut self, _conn: &Connection, _qh: &QueueHandle, _touch: &WlTouch, _time: u32, id: i32, position: ( f64, f64 ), ) { let focus = *self.touch_focus.get( &id ).unwrap_or( &SurfaceFocus::Main ); let pp = self.surface( focus ).to_physical( position.0, position.1 ); let is_primary = { let ss = self.surface( focus ); ss.primary_touch_id == Some( id ) }; if !is_primary { // Auxiliary slot: cache the latest position so `up`'s // release point is accurate, then surface the raw motion. { let ss = self.surface_mut( focus ); ss.touch_slots.insert( id, pp ); } self.app.on_touch_move( id as i64, pp.x, pp.y ); return; } self.pointer_pos = pp; let global_drag = self.has_active_long_press_drag(); let swipe = self.swipe_config( focus ); let outcome = { let ss = self.surface_mut( focus ); ss.gesture.on_move( pp, &ss.frame.widget_rects, &mut ss.frame.scroll_offsets, &swipe, global_drag ) }; self.apply_move_outcome( focus, outcome ); // Drag-to-select inside a TextEdit (touch path). let pressed_text = self.surface( focus ).gesture.pressed_idx .and_then( |idx| { let is_text = find_handlers( &self.surface( focus ).frame.widget_rects, idx ) .map( |h| h.is_text_input() ).unwrap_or( false ); if is_text { Some( idx ) } else { None } } ); if let Some( idx ) = pressed_text { self.handle_text_pointer_drag( focus, idx, pp ); } } fn shape( &mut self, _conn: &Connection, _qh: &QueueHandle, _touch: &WlTouch, _id: i32, _major: f64, _minor: f64, ) {} fn orientation( &mut self, _conn: &Connection, _qh: &QueueHandle, _touch: &WlTouch, _id: i32, _orientation: f64, ) {} fn cancel( &mut self, _conn: &Connection, _qh: &QueueHandle, _touch: &WlTouch ) { self.reset_touch_state(); } } impl AppData { /// Drop every in-flight touch gesture across all surfaces, notifying /// the app of any auxiliary slots that were still down. Shared by the /// `wl_touch.cancel` handler and the touch-capability add / remove path /// (suspend / resume on devices that power the touchscreen down): a /// yanked capability never delivers the pending `up` / `cancel`, so /// without this the stale `primary_touch_id` / gesture state strands and /// the first gesture after resume is misrouted and lost. Returns `true` /// when it actually had state to clear. pub( crate ) fn reset_touch_state( &mut self ) -> bool { let had_state = self.main.primary_touch_id.is_some() || !self.main.touch_slots.is_empty() || !self.touch_focus.is_empty() || self.overlays.values().any( |ss| ss.primary_touch_id.is_some() || !ss.touch_slots.is_empty() ); // Snapshot every auxiliary slot's last position so the app can be // notified with a meaningful release point, then drop every // per-surface slot in one pass. let mut aux_releases: Vec<( i32, crate::types::Point )> = Vec::new(); let collect = |ss: &mut SurfaceState, out: &mut Vec<( i32, crate::types::Point )>| { for ( id, pos ) in ss.touch_slots.drain() { out.push( ( id, pos ) ); } ss.primary_touch_id = None; ss.gesture.on_cancel(); }; collect( &mut self.main, &mut aux_releases ); for ss in self.overlays.values_mut() { collect( ss, &mut aux_releases ); } self.touch_focus.clear(); for ( id, pos ) in aux_releases { self.app.on_touch_up( id as i64, pos.x, pos.y ); } self.stop_button_repeat(); had_state } }