ui_events_winit/
lib.rs

1// Copyright 2025 the UI Events Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! This crate bridges [`winit`]'s native input events (mouse, touch, keyboard, etc.)
5//! into the [`ui-events`] model.
6//!
7//! The primary entry point is [`WindowEventReducer`].
8//!
9//! [`ui-events`]: https://docs.rs/ui-events/
10
11// LINEBENDER LINT SET - lib.rs - v3
12// See https://linebender.org/wiki/canonical-lints/
13// These lints shouldn't apply to examples or tests.
14#![cfg_attr(not(test), warn(unused_crate_dependencies))]
15// These lints shouldn't apply to examples.
16#![warn(clippy::print_stdout, clippy::print_stderr)]
17// Targeting e.g. 32-bit means structs containing usize can give false positives for 64-bit.
18#![cfg_attr(target_pointer_width = "64", warn(clippy::trivially_copy_pass_by_ref))]
19// END LINEBENDER LINT SET
20#![no_std]
21
22pub mod keyboard;
23pub mod pointer;
24
25extern crate alloc;
26use alloc::{vec, vec::Vec};
27
28#[cfg(not(target_arch = "wasm32"))]
29extern crate std;
30
31#[cfg(not(target_arch = "wasm32"))]
32pub use std::time::Instant;
33
34#[cfg(target_arch = "wasm32")]
35pub use web_time::Instant;
36
37use ui_events::{
38    ScrollDelta,
39    keyboard::KeyboardEvent,
40    pointer::{
41        PointerButtonEvent, PointerEvent, PointerGesture, PointerGestureEvent, PointerId,
42        PointerInfo, PointerScrollEvent, PointerState, PointerType, PointerUpdate,
43    },
44};
45use winit::{
46    event::{ElementState, Force, MouseScrollDelta, Touch, TouchPhase, WindowEvent},
47    keyboard::ModifiersState,
48};
49
50/// Manages stateful transformations of winit [`WindowEvent`].
51///
52/// Store a single instance of this per window, then call [`WindowEventReducer::reduce`]
53/// on each [`WindowEvent`] for that window.
54/// Use the [`WindowEventTranslation`] value to receive [`PointerEvent`]s and [`KeyboardEvent`]s.
55///
56/// This handles:
57///  - [`ModifiersChanged`][`WindowEvent::ModifiersChanged`]
58///  - [`KeyboardInput`][`WindowEvent::KeyboardInput`]
59///  - [`Touch`][`WindowEvent::Touch`]
60///  - [`MouseInput`][`WindowEvent::MouseInput`]
61///  - [`MouseWheel`][`WindowEvent::MouseWheel`]
62///  - [`CursorMoved`][`WindowEvent::CursorMoved`]
63///  - [`CursorEntered`][`WindowEvent::CursorEntered`]
64///  - [`CursorLeft`][`WindowEvent::CursorLeft`]
65///  - [`PinchGesture`][`WindowEvent::PinchGesture`]
66///  - [`RotationGesture`][`WindowEvent::RotationGesture`]
67#[derive(Debug, Default)]
68pub struct WindowEventReducer {
69    /// State of modifiers.
70    modifiers: ModifiersState,
71    /// State of the primary mouse pointer.
72    primary_state: PointerState,
73    /// Click and tap counter.
74    counter: TapCounter,
75    /// First time an event was received..
76    first_instant: Option<Instant>,
77}
78
79#[allow(
80    clippy::cast_possible_truncation,
81    reason = "There is no alternative to truncation here."
82)]
83impl WindowEventReducer {
84    /// Process a [`WindowEvent`].
85    pub fn reduce(
86        &mut self,
87        scale_factor: f64,
88        we: &WindowEvent,
89    ) -> Option<WindowEventTranslation> {
90        const PRIMARY_MOUSE: PointerInfo = PointerInfo {
91            pointer_id: Some(PointerId::PRIMARY),
92            // TODO: Maybe transmute device.
93            persistent_device_id: None,
94            pointer_type: PointerType::Mouse,
95        };
96
97        let time = Instant::now()
98            .duration_since(*self.first_instant.get_or_insert_with(Instant::now))
99            .as_nanos() as u64;
100
101        self.primary_state.time = time;
102        self.primary_state.scale_factor = scale_factor;
103
104        match we {
105            WindowEvent::ModifiersChanged(m) => {
106                self.modifiers = m.state();
107                self.primary_state.modifiers = keyboard::from_winit_modifier_state(self.modifiers);
108                None
109            }
110            WindowEvent::KeyboardInput { event, .. } => Some(WindowEventTranslation::Keyboard(
111                keyboard::from_winit_keyboard_event(event.clone(), self.modifiers),
112            )),
113            WindowEvent::CursorEntered { .. } => Some(WindowEventTranslation::Pointer(
114                PointerEvent::Enter(PRIMARY_MOUSE),
115            )),
116            WindowEvent::CursorLeft { .. } => Some(WindowEventTranslation::Pointer(
117                PointerEvent::Leave(PRIMARY_MOUSE),
118            )),
119            WindowEvent::CursorMoved { position, .. } => {
120                self.primary_state.position = *position;
121
122                Some(WindowEventTranslation::Pointer(self.counter.attach_count(
123                    scale_factor,
124                    PointerEvent::Move(PointerUpdate {
125                        pointer: PRIMARY_MOUSE,
126                        current: self.primary_state.clone(),
127                        coalesced: vec![],
128                        predicted: vec![],
129                    }),
130                )))
131            }
132            WindowEvent::MouseInput {
133                state: ElementState::Pressed,
134                button,
135                ..
136            } => {
137                let button = pointer::try_from_winit_button(*button);
138                if let Some(button) = button {
139                    self.primary_state.buttons.insert(button);
140                }
141
142                Some(WindowEventTranslation::Pointer(self.counter.attach_count(
143                    scale_factor,
144                    PointerEvent::Down(PointerButtonEvent {
145                        pointer: PRIMARY_MOUSE,
146                        button,
147                        state: self.primary_state.clone(),
148                    }),
149                )))
150            }
151            WindowEvent::MouseInput {
152                state: ElementState::Released,
153                button,
154                ..
155            } => {
156                let button = pointer::try_from_winit_button(*button);
157                if let Some(button) = button {
158                    self.primary_state.buttons.remove(button);
159                }
160
161                Some(WindowEventTranslation::Pointer(self.counter.attach_count(
162                    scale_factor,
163                    PointerEvent::Up(PointerButtonEvent {
164                        pointer: PRIMARY_MOUSE,
165                        button,
166                        state: self.primary_state.clone(),
167                    }),
168                )))
169            }
170            WindowEvent::MouseWheel { delta, .. } => Some(WindowEventTranslation::Pointer(
171                PointerEvent::Scroll(PointerScrollEvent {
172                    pointer: PRIMARY_MOUSE,
173                    delta: match *delta {
174                        MouseScrollDelta::LineDelta(x, y) => ScrollDelta::LineDelta(x, y),
175                        MouseScrollDelta::PixelDelta(p) => ScrollDelta::PixelDelta(p),
176                    },
177                    state: self.primary_state.clone(),
178                }),
179            )),
180            // Winit documentation says delta can be NaN; that is totally useless, so discard.
181            WindowEvent::PinchGesture { delta, .. } if delta.is_finite() => Some(
182                WindowEventTranslation::Pointer(PointerEvent::Gesture(PointerGestureEvent {
183                    pointer: PRIMARY_MOUSE,
184                    gesture: PointerGesture::Pinch(*delta as f32),
185                    state: self.primary_state.clone(),
186                })),
187            ),
188            // Winit documentation says delta can be NaN; that is totally useless, so discard.
189            WindowEvent::RotationGesture { delta, .. } if delta.is_finite() => {
190                Some(WindowEventTranslation::Pointer(PointerEvent::Gesture(
191                    PointerGestureEvent {
192                        pointer: PRIMARY_MOUSE,
193                        // Winit gives this in counterclockwise degrees.
194                        gesture: PointerGesture::Rotate((-*delta).to_radians()),
195                        state: self.primary_state.clone(),
196                    },
197                )))
198            }
199            WindowEvent::Touch(Touch {
200                phase,
201                id,
202                location,
203                force,
204                ..
205            }) => {
206                let pointer = PointerInfo {
207                    pointer_id: PointerId::new(id.saturating_add(1)),
208                    pointer_type: PointerType::Touch,
209                    persistent_device_id: None,
210                };
211
212                use TouchPhase::*;
213
214                let state = PointerState {
215                    time,
216                    position: *location,
217                    modifiers: self.primary_state.modifiers,
218                    pressure: if matches!(phase, Ended | Cancelled) {
219                        0.0
220                    } else {
221                        match force {
222                            Some(Force::Calibrated { force, .. }) => (force * 0.5) as f32,
223                            Some(Force::Normalized(q)) => *q as f32,
224                            _ => 0.5,
225                        }
226                    },
227                    ..Default::default()
228                };
229
230                Some(WindowEventTranslation::Pointer(self.counter.attach_count(
231                    scale_factor,
232                    match phase {
233                        Started => PointerEvent::Down(PointerButtonEvent {
234                            pointer,
235                            button: None,
236                            state,
237                        }),
238                        Moved => PointerEvent::Move(PointerUpdate {
239                            pointer,
240                            current: state,
241                            coalesced: vec![],
242                            predicted: vec![],
243                        }),
244                        Cancelled => PointerEvent::Cancel(pointer),
245                        Ended => PointerEvent::Up(PointerButtonEvent {
246                            pointer,
247                            button: None,
248                            state,
249                        }),
250                    },
251                )))
252            }
253            _ => None,
254        }
255    }
256}
257
258/// Result of [`WindowEventReducer::reduce`].
259#[derive(Debug)]
260pub enum WindowEventTranslation {
261    /// Resulting [`KeyboardEvent`].
262    Keyboard(KeyboardEvent),
263    /// Resulting [`PointerEvent`].
264    Pointer(PointerEvent),
265}
266
267#[derive(Clone, Debug)]
268struct TapState {
269    /// Pointer ID used to attach tap counts to [`PointerEvent::Move`].
270    pointer_id: Option<PointerId>,
271    /// Nanosecond timestamp when the tap went Down.
272    down_time: u64,
273    /// Nanosecond timestamp when the tap went Up.
274    ///
275    /// Resets to `down_time` when tap goes Down.
276    up_time: u64,
277    /// The local tap count as of the last Down phase.
278    count: u8,
279    /// x coordinate.
280    x: f64,
281    /// y coordinate.
282    y: f64,
283}
284
285#[derive(Debug, Default)]
286struct TapCounter {
287    taps: Vec<TapState>,
288}
289
290impl TapCounter {
291    /// Enhance a [`PointerEvent`] with a `count`.
292    fn attach_count(&mut self, scale_factor: f64, e: PointerEvent) -> PointerEvent {
293        match e {
294            PointerEvent::Down(mut event) => {
295                let pointer_id = event.pointer.pointer_id;
296                let position = event.state.position;
297                let time = event.state.time;
298
299                let slop = match event.pointer.pointer_type {
300                    // This is on the low side of double tap slop, validated
301                    // experimentally to work on a few touchscreen laptops.
302                    PointerType::Touch => 12.0,
303                    PointerType::Pen => 6.0,
304                    // This is slightly more forgiving than the default on Windows for mice.
305                    // In order to make the slop calculation more similar between devices,
306                    // this uses a slightly different method than Windows, which tests if the
307                    // tap is in a box, rather than in a circle, centered on the anchor point.
308                    _ => 2.0,
309                } * core::f64::consts::SQRT_2
310                    * scale_factor;
311
312                if let Some(tap) =
313                    self.taps.iter_mut().find(|TapState { x, y, up_time, .. }| {
314                        let dx = (x - position.x).abs();
315                        let dy = (y - position.y).abs();
316                        (dx * dx + dy * dy).sqrt() < slop && (up_time + 500_000_000) > time
317                    })
318                {
319                    let count = tap.count + 1;
320                    event.state.count = count;
321                    tap.count = count;
322                    tap.pointer_id = pointer_id;
323                    tap.down_time = time;
324                    tap.up_time = time;
325                    tap.x = position.x;
326                    tap.y = position.y;
327                } else {
328                    let s = TapState {
329                        pointer_id,
330                        down_time: time,
331                        up_time: time,
332                        count: 1,
333                        x: position.x,
334                        y: position.y,
335                    };
336                    if let Some(t) = self
337                        .taps
338                        .iter_mut()
339                        .find(|state| state.pointer_id == pointer_id)
340                    {
341                        *t = s;
342                    } else {
343                        self.taps.push(s);
344                    }
345                    event.state.count = 1;
346                };
347                self.clear_expired(time);
348                PointerEvent::Down(event)
349            }
350            PointerEvent::Up(mut event) => {
351                let p_id = event.pointer.pointer_id;
352                if let Some(tap) = self.taps.iter_mut().find(|state| state.pointer_id == p_id) {
353                    tap.up_time = event.state.time;
354                    event.state.count = tap.count;
355                }
356                PointerEvent::Up(event)
357            }
358            PointerEvent::Move(PointerUpdate {
359                pointer,
360                mut current,
361                mut coalesced,
362                mut predicted,
363            }) => {
364                if let Some(TapState { count, .. }) = self
365                    .taps
366                    .iter()
367                    .find(
368                        |TapState {
369                             pointer_id,
370                             down_time,
371                             up_time,
372                             ..
373                         }| {
374                            *pointer_id == pointer.pointer_id && down_time == up_time
375                        },
376                    )
377                    .cloned()
378                {
379                    current.count = count;
380                    for event in coalesced.iter_mut() {
381                        event.count = count;
382                    }
383                    for event in predicted.iter_mut() {
384                        event.count = count;
385                    }
386                    PointerEvent::Move(PointerUpdate {
387                        pointer,
388                        current,
389                        coalesced,
390                        predicted,
391                    })
392                } else {
393                    PointerEvent::Move(PointerUpdate {
394                        pointer,
395                        current,
396                        coalesced,
397                        predicted,
398                    })
399                }
400            }
401            PointerEvent::Cancel(p) => {
402                self.taps
403                    .retain(|TapState { pointer_id, .. }| *pointer_id != p.pointer_id);
404                PointerEvent::Cancel(p)
405            }
406            PointerEvent::Leave(p) => {
407                self.taps
408                    .retain(|TapState { pointer_id, .. }| *pointer_id != p.pointer_id);
409                PointerEvent::Leave(p)
410            }
411            e
412            @ (PointerEvent::Enter(..) | PointerEvent::Scroll(..) | PointerEvent::Gesture(..)) => e,
413        }
414    }
415
416    /// Clear expired taps.
417    ///
418    /// `t` is the time of the last received event.
419    /// All events have the same time base on Android, so this is valid here.
420    fn clear_expired(&mut self, t: u64) {
421        self.taps.retain(
422            |TapState {
423                 down_time, up_time, ..
424             }| { down_time == up_time || (up_time + 500_000_000) > t },
425        );
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    // CI will fail unless cargo nextest can execute at least one test per workspace.
432    // Delete this dummy test once we have an actual real test.
433    #[test]
434    fn dummy_test_until_we_have_a_real_test() {}
435}