// 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 only touch-specific state is `AppData::touch_focus`, a //! `HashMap` so a finger that landed on an //! overlay continues to route to that overlay even if it drifts over //! the main surface. Multi-finger tracking is not yet modelled — the //! gesture machine is single-gesture; a second finger arriving while //! the first is pressed overwrites the same slot. Good enough for //! sliders, swipes and taps; a proper multi-touch rewrite is a //! separate refactor. 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; 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.widget_rects, &ss.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 ).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 ).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 ); // 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.gesture.on_release( pos, &ss.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 ); 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.widget_rects, &mut ss.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 ).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.touch_focus.clear(); // The compositor is stealing every active touch — drop all // in-flight gesture state across every surface. let clear = |ss: &mut SurfaceState| { ss.gesture.on_cancel(); }; clear( &mut self.main ); for ss in self.overlays.values_mut() { clear( ss ); } self.stop_button_repeat(); } }