ravenrs_wayland_gestures/
lib.rs1use std::ffi::c_void;
9use std::ptr::NonNull;
10
11#[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 pub fn begin(&mut self) {
26 self.last = 1.0;
27 }
28
29 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 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 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 pub(super) fn listen<F>(display: NonNull<c_void>, on_pinch: F) -> Result<(), String>
165 where
166 F: Fn(f32) + Send + 'static,
167 {
168 #[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
190pub 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}