Skip to main content

ravenrs_wayland_gestures/
lib.rs

1//! Touchpad pinches on Wayland. winit 0.30 delivers them only on macOS and iOS, so this
2//! listens for them on the connection winit already has: gestures go to the client that owns
3//! the window, and a connection of our own would never hear of them.
4//!
5//! This crate holds the only `unsafe` call in the workspace, and goes away with a winit that
6//! delivers these gestures on Wayland.
7
8use std::ffi::c_void;
9use std::ptr::NonNull;
10
11/// Turns the total scale a gesture reports into how much it changed since the last event.
12#[derive(Clone, Copy, Debug)]
13pub struct PinchTracker {
14    last: f64,
15}
16
17impl Default for PinchTracker {
18    fn default() -> Self {
19        Self { last: 1.0 }
20    }
21}
22
23impl PinchTracker {
24    /// A gesture began: scales count from here.
25    pub fn begin(&mut self) {
26        self.last = 1.0;
27    }
28
29    /// The gesture stands at this scale. Returns the change as a fraction, `0.1` for 10% bigger.
30    pub fn update(&mut self, scale: f64) -> f32 {
31        if scale <= 0.0 || !scale.is_finite() {
32            return 0.0;
33        }
34        let change = scale / self.last.max(f64::EPSILON) - 1.0;
35        self.last = scale;
36        change as f32
37    }
38}
39
40#[cfg(any(
41    target_os = "linux",
42    target_os = "dragonfly",
43    target_os = "freebsd",
44    target_os = "netbsd",
45    target_os = "openbsd"
46))]
47mod listener {
48    use wayland_backend::client::Backend;
49    use wayland_client::protocol::{wl_pointer, wl_registry, wl_seat};
50    use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, event_created_child};
51    use wayland_protocols::wp::pointer_gestures::zv1::client::{
52        zwp_pointer_gesture_pinch_v1 as pinch, zwp_pointer_gestures_v1 as gestures,
53    };
54
55    use super::{NonNull, PinchTracker, c_void};
56
57    /// The version of the protocol this asks for: the first has everything it needs.
58    const GESTURES_VERSION: u32 = 1;
59    const SEAT_VERSION: u32 = 1;
60
61    struct Listening<F> {
62        on_pinch: F,
63        tracker: PinchTracker,
64        seat: Option<wl_seat::WlSeat>,
65        gestures: Option<gestures::ZwpPointerGesturesV1>,
66        /// Kept alive while it is being listened to.
67        held: Option<(wl_pointer::WlPointer, pinch::ZwpPointerGesturePinchV1)>,
68    }
69
70    impl<F: Fn(f32) + 'static> Listening<F> {
71        fn connect(&mut self, qh: &QueueHandle<Self>) {
72            if self.held.is_some() {
73                return;
74            }
75            if let (Some(seat), Some(gestures)) = (&self.seat, &self.gestures) {
76                let pointer = seat.get_pointer(qh, ());
77                let pinch = gestures.get_pinch_gesture(&pointer, qh, ());
78                self.held = Some((pointer, pinch));
79            }
80        }
81    }
82
83    impl<F: Fn(f32) + 'static> Dispatch<wl_registry::WlRegistry, ()> for Listening<F> {
84        fn event(
85            state: &mut Self,
86            registry: &wl_registry::WlRegistry,
87            event: wl_registry::Event,
88            (): &(),
89            _: &Connection,
90            qh: &QueueHandle<Self>,
91        ) {
92            if let wl_registry::Event::Global { name, interface, .. } = event {
93                if interface == wl_seat::WlSeat::interface().name && state.seat.is_none() {
94                    state.seat = Some(registry.bind(name, SEAT_VERSION, qh, ()));
95                } else if interface == gestures::ZwpPointerGesturesV1::interface().name {
96                    state.gestures = Some(registry.bind(name, GESTURES_VERSION, qh, ()));
97                }
98                state.connect(qh);
99            }
100        }
101    }
102
103    impl<F: Fn(f32) + 'static> Dispatch<wl_seat::WlSeat, ()> for Listening<F> {
104        fn event(
105            _: &mut Self,
106            _: &wl_seat::WlSeat,
107            _: wl_seat::Event,
108            (): &(),
109            _: &Connection,
110            _: &QueueHandle<Self>,
111        ) {
112        }
113    }
114
115    impl<F: Fn(f32) + 'static> Dispatch<wl_pointer::WlPointer, ()> for Listening<F> {
116        fn event(
117            _: &mut Self,
118            _: &wl_pointer::WlPointer,
119            _: wl_pointer::Event,
120            (): &(),
121            _: &Connection,
122            _: &QueueHandle<Self>,
123        ) {
124        }
125    }
126
127    impl<F: Fn(f32) + 'static> Dispatch<gestures::ZwpPointerGesturesV1, ()> for Listening<F> {
128        fn event(
129            _: &mut Self,
130            _: &gestures::ZwpPointerGesturesV1,
131            _: gestures::Event,
132            (): &(),
133            _: &Connection,
134            _: &QueueHandle<Self>,
135        ) {
136        }
137
138        event_created_child!(Listening<F>, gestures::ZwpPointerGesturesV1, []);
139    }
140
141    impl<F: Fn(f32) + 'static> Dispatch<pinch::ZwpPointerGesturePinchV1, ()> for Listening<F> {
142        fn event(
143            state: &mut Self,
144            _: &pinch::ZwpPointerGesturePinchV1,
145            event: pinch::Event,
146            (): &(),
147            _: &Connection,
148            _: &QueueHandle<Self>,
149        ) {
150            match event {
151                pinch::Event::Begin { .. } => state.tracker.begin(),
152                pinch::Event::Update { scale, .. } => {
153                    let change = state.tracker.update(scale);
154                    if change != 0.0 {
155                        (state.on_pinch)(change);
156                    }
157                }
158                _ => {}
159            }
160        }
161    }
162
163    /// See [`crate::listen`].
164    pub(super) fn listen<F>(display: NonNull<c_void>, on_pinch: F) -> Result<(), String>
165    where
166        F: Fn(f32) + Send + 'static,
167    {
168        // SAFETY: `listen` requires a live `wl_display` that outlives the program's use of it.
169        // libwayland is made for several queues over one display, and this one is our own.
170        #[allow(unsafe_code)]
171        let backend = unsafe { Backend::from_foreign_display(display.as_ptr().cast()) };
172        let connection = Connection::from_backend(backend);
173        let mut queue = connection.new_event_queue();
174        let qh = queue.handle();
175        let _registry = connection.display().get_registry(&qh, ());
176        let mut state =
177            Listening { on_pinch, tracker: PinchTracker::default(), seat: None, gestures: None, held: None };
178        queue.roundtrip(&mut state).map_err(|e| e.to_string())?;
179        if state.gestures.is_none() {
180            return Err("the compositor offers no pointer gestures".to_owned());
181        }
182        std::thread::Builder::new()
183            .name("ravenrs-wayland-gestures".to_owned())
184            .spawn(move || while queue.blocking_dispatch(&mut state).is_ok() {})
185            .map(|_| ())
186            .map_err(|e| e.to_string())
187    }
188}
189
190/// Calls `on_pinch` on another thread with how much each touchpad pinch changed, as a
191/// fraction. Off Wayland's systems it does nothing.
192///
193/// `display` must be the live `wl_display` of the program's windows, for as long as the
194/// program runs. Fails if the compositor has no pointer gestures.
195pub fn listen<F>(display: NonNull<c_void>, on_pinch: F) -> Result<(), String>
196where
197    F: Fn(f32) + Send + 'static,
198{
199    #[cfg(any(
200        target_os = "linux",
201        target_os = "dragonfly",
202        target_os = "freebsd",
203        target_os = "netbsd",
204        target_os = "openbsd"
205    ))]
206    {
207        listener::listen(display, on_pinch)
208    }
209    #[cfg(not(any(
210        target_os = "linux",
211        target_os = "dragonfly",
212        target_os = "freebsd",
213        target_os = "netbsd",
214        target_os = "openbsd"
215    )))]
216    {
217        let _ = (display, on_pinch);
218        Ok(())
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::PinchTracker;
225
226    #[test]
227    fn the_total_scale_becomes_changes_that_add_up_to_it() {
228        let mut tracker = PinchTracker::default();
229        tracker.begin();
230        let mut zoom = 1.0_f32;
231        for scale in [1.1, 1.3, 1.5] {
232            zoom *= 1.0 + tracker.update(scale);
233        }
234        assert!((zoom - 1.5).abs() < 0.001, "{zoom}");
235    }
236
237    #[test]
238    fn a_new_gesture_counts_from_one_again() {
239        let mut tracker = PinchTracker::default();
240        tracker.update(2.0);
241        tracker.begin();
242        assert!((tracker.update(1.1) - 0.1).abs() < 0.001);
243    }
244
245    #[test]
246    fn a_broken_scale_changes_nothing() {
247        let mut tracker = PinchTracker::default();
248        assert_eq!(tracker.update(0.0), 0.0);
249        assert_eq!(tracker.update(f64::NAN), 0.0);
250    }
251}