ltk: opt-in raw touch stream for apps that consume input themselves
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Add App::claims_raw_touch(), an opt-in hook that routes the primary finger through the raw on_touch_down/move/up callbacks instead of the built-in single-slot gesture machine. Until now the primary finger was consumed entirely by the widget gesture machine (taps, swipes, presses) and never reached the app, which made it impossible to drive a self-contained input consumer like an embedded WebView or a game canvas from a touchscreen: the widget tree saw the taps, the embedded content never did. With the hook active every finger surfaces verbatim through the raw stream, widget gestures never arm, and per-finger positions are cached in SurfaceState::touch_slots for all slots so wl_touch.up (which carries no coordinates) can still report a release point.
The touch module doc and the on_touch_down docs are updated to describe the new contract; the default remains unchanged for every existing app.
Pin the transitive dependency ignore to 0.4.23: rust-i18n pulls it via globwalk, 0.4.24+ requires a rustc newer than Debian's 1.85, and its manifest does not declare that MSRV, so a fresh resolution breaks the build on stable.
This commit is contained in:
2026-07-21 09:56:05 +02:00
parent 8762ab9ce0
commit 3d7f3c53de
3 changed files with 55 additions and 1 deletions

View File

@@ -43,6 +43,9 @@ rustybuzz = "0.14"
image = { version = "=0.25.9", default-features = false, features = ["png", "jpeg", "webp"] } image = { version = "=0.25.9", default-features = false, features = ["png", "jpeg", "webp"] }
resvg = "0.44" resvg = "0.44"
rust-i18n = "3" rust-i18n = "3"
# Transitive dep of rust-i18n (globwalk → ignore); 0.4.24+ needs a rustc
# newer than Debian's 1.85, and its manifest does not declare that MSRV.
ignore = "=0.4.23"
wayland-protocols = { version = "0.32", features = ["client", "unstable", "staging"] } wayland-protocols = { version = "0.32", features = ["client", "unstable", "staging"] }
wayland-egl = "0.32" wayland-egl = "0.32"
khronos-egl = { version = "6", features = ["dynamic"] } khronos-egl = { version = "6", features = ["dynamic"] }

View File

@@ -602,6 +602,10 @@ pub trait App: 'static
/// built-in single-slot machine cannot model. `(x, y)` are in /// built-in single-slot machine cannot model. `(x, y)` are in
/// physical pixels, matching the coordinate space of /// physical pixels, matching the coordinate space of
/// [`Self::on_pointer_move`]. /// [`Self::on_pointer_move`].
///
/// When [`Self::claims_raw_touch`] returns `true` the primary
/// finger arrives here as well, instead of driving the gesture
/// machine.
fn on_touch_down( &mut self, _id: i64, _x: f32, _y: f32 ) {} fn on_touch_down( &mut self, _id: i64, _x: f32, _y: f32 ) {}
/// See [`Self::on_touch_down`]. /// See [`Self::on_touch_down`].
fn on_touch_move( &mut self, _id: i64, _x: f32, _y: f32 ) {} fn on_touch_move( &mut self, _id: i64, _x: f32, _y: f32 ) {}
@@ -609,6 +613,14 @@ pub trait App: 'static
/// position of the finger (`wl_touch.up` does not carry one). /// position of the finger (`wl_touch.up` does not carry one).
fn on_touch_up( &mut self, _id: i64, _x: f32, _y: f32 ) {} fn on_touch_up( &mut self, _id: i64, _x: f32, _y: f32 ) {}
/// Return `true` to receive the *primary* finger through the raw
/// [`Self::on_touch_down`] / move / up stream as well, bypassing
/// the built-in single-slot gesture machine entirely — widget
/// presses, taps and swipes stop working on touch. For apps whose
/// surface is one self-contained input consumer (an embedded
/// WebView, a game canvas) that needs every finger verbatim.
fn claims_raw_touch( &self ) -> bool { false }
/// Pointer of a cross-application drag-and-drop entered or moved /// Pointer of a cross-application drag-and-drop entered or moved
/// inside the main surface. `(x, y)` is in logical pixels. /// inside the main surface. `(x, y)` is in logical pixels.
fn on_drop_motion( &mut self, _x: f32, _y: f32 ) {} fn on_drop_motion( &mut self, _x: f32, _y: f32 ) {}

View File

@@ -26,6 +26,13 @@
//! per touch id, so an auxiliary slot that landed on an overlay //! per touch id, so an auxiliary slot that landed on an overlay
//! keeps reporting through that overlay's `App` callback even if //! keeps reporting through that overlay's `App` callback even if
//! the finger drifts over the main surface. //! 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::seat::touch::TouchHandler;
use smithay_client_toolkit::reexports::client:: use smithay_client_toolkit::reexports::client::
@@ -58,6 +65,16 @@ impl<A: App> TouchHandler for AppData<A>
let pos = self.surface( focus ).to_physical( position.0, position.1 ); let pos = self.surface( focus ).to_physical( position.0, position.1 );
self.pointer_pos = pos; 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 // Compositor re-grab after the origin surface died mid-drag: this
// slot is already our primary and the drag state was migrated here, // slot is already our primary and the drag state was migrated here,
// so treat the redundant `down` as a continuation, not a restart. // so treat the redundant `down` as a continuation, not a restart.
@@ -178,6 +195,16 @@ impl<A: App> TouchHandler for AppData<A>
) )
{ {
let focus = self.touch_focus.remove( &id ).unwrap_or( SurfaceFocus::Main ); 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 // Auxiliary release: not the primary slot → recover the last
// recorded position for the up callback (Wayland's `wl_touch.up` // recorded position for the up callback (Wayland's `wl_touch.up`
// does not carry one) and bail before reaching the gesture // does not carry one) and bail before reaching the gesture
@@ -227,6 +254,17 @@ impl<A: App> TouchHandler for AppData<A>
let focus = *self.touch_focus.get( &id ).unwrap_or( &SurfaceFocus::Main ); let focus = *self.touch_focus.get( &id ).unwrap_or( &SurfaceFocus::Main );
let pp = self.surface( focus ).to_physical( position.0, position.1 ); 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 is_primary =
{ {
let ss = self.surface( focus ); let ss = self.surface( focus );
@@ -296,7 +334,8 @@ impl<A: App> TouchHandler for AppData<A>
impl<A: App> AppData<A> impl<A: App> AppData<A>
{ {
/// Drop every in-flight touch gesture across all surfaces, notifying /// Drop every in-flight touch gesture across all surfaces, notifying
/// the app of any auxiliary slots that were still down. Shared by the /// 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 /// `wl_touch.cancel` handler and the touch-capability add / remove path
/// (suspend / resume on devices that power the touchscreen down): a /// (suspend / resume on devices that power the touchscreen down): a
/// yanked capability never delivers the pending `up` / `cancel`, so /// yanked capability never delivers the pending `up` / `cancel`, so