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