From 42e5ff66da8276a67248bf0a62a235f2046d2884 Mon Sep 17 00:00:00 2001 From: Jon Kinney Date: Wed, 3 Jun 2026 12:04:56 -0500 Subject: [PATCH] winit-wayland: add hold gesture support Bind the hold gesture from zwp_pointer_gestures_v1 (added in v3 of the manager) alongside the existing pinch gesture, emitting the new WindowEvent::HoldGesture on begin (Started) and end (Ended/Cancelled). A hold gesture fires when fingers are placed on a touchpad without significant movement; per the protocol its canonical use is to stop kinetic ("momentum") scrolling. The manager is now bound up to v3 so the hold gesture is available where the compositor supports it; older compositors bind a lower version and keep the pinch gesture, and the hold object is only created when the bound manager is v3 or newer. --- winit-core/src/event.rs | 15 ++++++ winit-wayland/src/seat/mod.rs | 21 ++++++++ .../src/seat/pointer/pointer_gesture.rs | 52 ++++++++++++++++++- winit/src/changelog/unreleased.md | 1 + 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index e6500c2133..1a2ba0dec6 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -298,6 +298,21 @@ pub enum WindowEvent { phase: TouchPhase, }, + /// Multi-finger hold gesture. + /// + /// Sent when fingers are placed on a touchpad without significant movement + /// (`Started`) and again when they lift or the gesture is cancelled + /// (`Ended` / `Cancelled`). The canonical use is to stop kinetic + /// ("momentum") scrolling. + /// + /// ## Platform-specific + /// + /// - Only available on **Wayland** (via `zwp_pointer_gestures_v1`, v3+). + HoldGesture { + device_id: Option, + phase: TouchPhase, + }, + /// Double tap gesture. /// /// On a Mac, smart magnification is triggered by a double tap with two fingers diff --git a/winit-wayland/src/seat/mod.rs b/winit-wayland/src/seat/mod.rs index ddf0e061df..df2a51b2d7 100644 --- a/winit-wayland/src/seat/mod.rs +++ b/winit-wayland/src/seat/mod.rs @@ -12,6 +12,7 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3:: use sctk::seat::pointer::{ThemeSpec, ThemedPointer}; use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState}; use tracing::warn; +use wayland_protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_hold_v1::ZwpPointerGestureHoldV1; use wayland_protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1; use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_seat_v2::ZwpTabletSeatV2; use winit_core::event::WindowEvent; @@ -60,6 +61,9 @@ pub struct WinitSeatState { /// The pinch pointer gesture bound on the seat. pointer_gesture_pinch: Option, + /// The hold pointer gesture bound on the seat (v3+ compositors only). + pointer_gesture_hold: Option, + /// The keyboard bound on the seat. keyboard_state: Option, @@ -141,6 +145,19 @@ impl SeatHandler for WinitState { ) }); + // The hold gesture is only available from v3 of the manager. + seat_state.pointer_gesture_hold = self + .pointer_gestures + .as_ref() + .filter(|manager| manager.version() >= 3) + .map(|manager| { + manager.get_hold_gesture( + themed_pointer.pointer(), + queue_handle, + PointerGestureData::default(), + ) + }); + let themed_pointer = Arc::new(themed_pointer); // Register cursor surface. @@ -209,6 +226,10 @@ impl SeatHandler for WinitState { pointer_gesture_pinch.destroy(); } + if let Some(pointer_gesture_hold) = seat_state.pointer_gesture_hold.take() { + pointer_gesture_hold.destroy(); + } + if let Some(pointer) = seat_state.pointer.take() { let pointer_data = pointer.pointer().winit_data(); diff --git a/winit-wayland/src/seat/pointer/pointer_gesture.rs b/winit-wayland/src/seat/pointer/pointer_gesture.rs index 87b97d5d0c..a9d43ac15c 100644 --- a/winit-wayland/src/seat/pointer/pointer_gesture.rs +++ b/winit-wayland/src/seat/pointer/pointer_gesture.rs @@ -6,6 +6,9 @@ use sctk::compositor::SurfaceData; use sctk::globals::GlobalData; use sctk::reexports::client::globals::{BindError, GlobalList}; use sctk::reexports::client::{Connection, Dispatch, Proxy, QueueHandle, delegate_dispatch}; +use sctk::reexports::protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_hold_v1::{ + Event as HoldEvent, ZwpPointerGestureHoldV1, +}; use sctk::reexports::protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_pinch_v1::{ Event, ZwpPointerGesturePinchV1, }; @@ -27,7 +30,9 @@ impl PointerGesturesState { globals: &GlobalList, queue_handle: &QueueHandle, ) -> Result { - let pointer_gestures = globals.bind(queue_handle, 1..=1, GlobalData)?; + // Bind up to v3 so the hold gesture (added in v3) is available where the + // compositor supports it; older compositors bind lower and keep pinch. + let pointer_gestures = globals.bind(queue_handle, 1..=3, GlobalData)?; Ok(Self { pointer_gestures }) } } @@ -153,5 +158,50 @@ impl Dispatch for Poin } } +impl Dispatch for PointerGesturesState { + fn event( + state: &mut WinitState, + _proxy: &ZwpPointerGestureHoldV1, + event: ::Event, + data: &PointerGestureData, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + let mut pointer_gesture_data = data.inner.lock().unwrap(); + let (window_id, phase) = match event { + HoldEvent::Begin { surface, .. } => { + // Don't handle events from a subsurface. + if surface.data::().is_none_or(|data| data.parent_surface().is_some()) + { + return; + } + + let window_id = crate::make_wid(&surface); + pointer_gesture_data.window_id = Some(window_id); + + (window_id, TouchPhase::Started) + }, + HoldEvent::End { cancelled, .. } => { + let window_id = match pointer_gesture_data.window_id { + Some(window_id) => window_id, + _ => return, + }; + + // Reset the state. + *pointer_gesture_data = Default::default(); + + let phase = if cancelled == 0 { TouchPhase::Ended } else { TouchPhase::Cancelled }; + (window_id, phase) + }, + _ => unreachable!("Unknown event {event:?}"), + }; + + state + .events_sink + .push_window_event(WindowEvent::HoldGesture { device_id: None, phase }, window_id); + } +} + delegate_dispatch!(WinitState: [ZwpPointerGesturesV1: GlobalData] => PointerGesturesState); delegate_dispatch!(WinitState: [ZwpPointerGesturePinchV1: PointerGestureData] => PointerGesturesState); +delegate_dispatch!(WinitState: [ZwpPointerGestureHoldV1: PointerGestureData] => PointerGesturesState); diff --git a/winit/src/changelog/unreleased.md b/winit/src/changelog/unreleased.md index 729bfd639b..5544d2df0c 100644 --- a/winit/src/changelog/unreleased.md +++ b/winit/src/changelog/unreleased.md @@ -46,6 +46,7 @@ changelog entry. - On iOS, add Apple Pencil support with force, altitude, and azimuth data. - On Redox, add support for missing keyboard scancodes. - Implement `Send` and `Sync` for `OwnedDisplayHandle`. +- Add `WindowEvent::HoldGesture`, implemented on Wayland. - Use new macOS 15 cursors for resize icons. - On Android, added scancode conversions for more obscure key codes. - On Wayland, added ext-background-effect-v1 support.