Skip to main content

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