ltk/input/touch/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! 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.
//!
//! Apps that return `true` from [`App::claims_raw_touch`] opt out of
//! the primary-slot gesture machine entirely: every finger, primary
//! included, surfaces through the raw `on_touch_*` stream, and
//! widget presses, taps and swipes never fire. Positions are cached
//! in `SurfaceState::touch_slots` for all fingers in that mode so
//! `up` can report a release point.
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<A: App> TouchHandler for AppData<A>
{
fn down(
&mut self,
_conn: &Connection,
qh: &QueueHandle<Self>,
_touch: &WlTouch,
serial: u32,
_time: u32,
surface: WlSurface,
id: i32,
position: ( f64, f64 ),
)
{
self.last_input_serial = serial;
if self.is_secondary_surface( &surface ) { return; }
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;
// Raw-touch apps take every finger verbatim; the gesture
// machine never arms. Slots are tracked so `up` (which carries
// no position in wl_touch) can report the release point.
if self.app.claims_raw_touch()
{
self.surface_mut( focus ).touch_slots.insert( id, pos );
self.app.on_touch_down( id as i64, pos.x, pos.y );
return;
}
// 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<Self>,
_touch: &WlTouch,
_serial: u32,
_time: u32,
id: i32,
)
{
let focus = self.touch_focus.remove( &id ).unwrap_or( SurfaceFocus::Main );
if self.app.claims_raw_touch()
{
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;
}
// 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<Self>,
_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 );
if self.app.claims_raw_touch()
{
self.pointer_pos = pp;
{
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;
}
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<Self>,
_touch: &WlTouch,
_id: i32,
_major: f64,
_minor: f64,
) {}
fn orientation(
&mut self,
_conn: &Connection,
_qh: &QueueHandle<Self>,
_touch: &WlTouch,
_id: i32,
_orientation: f64,
) {}
fn cancel( &mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _touch: &WlTouch )
{
self.reset_touch_state();
}
}
impl<A: App> AppData<A>
{
/// Drop every in-flight touch gesture across all surfaces, notifying
/// the app of any slot still down that it was tracking raw (auxiliary
/// fingers, or every finger under `claims_raw_touch`). 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<A::Message>, 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
}
}