Skip to main content

astrelis_winit/
event.rs

1use astrelis_core::geometry::{LogicalPosition, LogicalSize, PhysicalPosition};
2use astrelis_core::profiling::profile_function;
3pub use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent as WinitEvent};
4pub use winit::keyboard::*;
5
6use std::collections::VecDeque;
7
8/// Touch phase for gesture state tracking.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum TouchPhase {
11    /// Touch or gesture started.
12    Started,
13    /// Touch or gesture moved/updated.
14    Moved,
15    /// Touch or gesture ended.
16    Ended,
17    /// Touch or gesture was cancelled.
18    Cancelled,
19}
20
21impl From<winit::event::TouchPhase> for TouchPhase {
22    fn from(phase: winit::event::TouchPhase) -> Self {
23        match phase {
24            winit::event::TouchPhase::Started => TouchPhase::Started,
25            winit::event::TouchPhase::Moved => TouchPhase::Moved,
26            winit::event::TouchPhase::Ended => TouchPhase::Ended,
27            winit::event::TouchPhase::Cancelled => TouchPhase::Cancelled,
28        }
29    }
30}
31
32/// Individual touch point event.
33#[derive(Debug, Clone)]
34pub struct TouchEvent {
35    /// Device ID that generated this touch.
36    pub device_id: u64,
37    /// Unique identifier for this touch point.
38    pub id: u64,
39    /// Current phase of the touch.
40    pub phase: TouchPhase,
41    /// Position of the touch in logical coordinates.
42    pub position: LogicalPosition<f64>,
43    /// Force of the touch (normalized 0.0 to 1.0), if available.
44    pub force: Option<f32>,
45}
46
47/// Pinch gesture for zoom operations.
48#[derive(Debug, Clone)]
49pub struct PinchGesture {
50    /// Scale delta: positive = magnify, negative = shrink.
51    pub delta: f64,
52    /// Current phase of the gesture.
53    pub phase: TouchPhase,
54}
55
56/// Rotation gesture for rotation operations.
57#[derive(Debug, Clone)]
58pub struct RotationGesture {
59    /// Rotation delta in radians: positive = counter-clockwise, negative = clockwise.
60    pub delta: f64,
61    /// Current phase of the gesture.
62    pub phase: TouchPhase,
63}
64
65/// Pan gesture for two-finger scrolling/panning.
66#[derive(Debug, Clone)]
67pub struct PanGesture {
68    /// Pan delta in logical coordinates.
69    pub delta: LogicalPosition<f64>,
70    /// Current phase of the gesture.
71    pub phase: TouchPhase,
72}
73
74/// Event queue with batching and deduplication
75pub struct EventQueue {
76    /// Pending events for this frame
77    pending: VecDeque<Event>,
78
79    /// High-priority events (processed first)
80    priority: VecDeque<Event>,
81
82    /// Deduplicated events (only last value kept)
83    latest_mouse_pos: Option<LogicalPosition<f64>>,
84    latest_scale_factor: Option<f64>,
85
86    /// Statistics
87    stats: EventStats,
88}
89
90impl EventQueue {
91    pub fn new() -> Self {
92        Self {
93            pending: VecDeque::with_capacity(64),
94            priority: VecDeque::with_capacity(8),
95            latest_mouse_pos: None,
96            latest_scale_factor: None,
97            stats: EventStats::default(),
98        }
99    }
100
101    /// Push event to queue (called from winit handler)
102    pub fn push(&mut self, event: Event) {
103        self.stats.events_received += 1;
104
105        match event {
106            // High priority - process immediately
107            Event::CloseRequested
108            | Event::WindowResized(_)
109            | Event::Focused(_)
110            | Event::ThemeChanged(_) => {
111                self.priority.push_back(event);
112            }
113
114            // Deduplicate - only keep latest
115            Event::MouseMoved(pos) => {
116                self.latest_mouse_pos = Some(pos);
117            }
118            Event::ScaleFactorChanged(scale) => {
119                self.latest_scale_factor = Some(scale);
120            }
121
122            // Normal priority
123            _ => {
124                self.pending.push_back(event);
125            }
126        }
127    }
128
129    /// Process all events and return batch
130    pub fn drain(&mut self) -> EventBatch {
131        let mut events = Vec::with_capacity(self.priority.len() + self.pending.len() + 2);
132
133        // Priority events first
134        events.extend(self.priority.drain(..));
135
136        // Deduplicated events
137        if let Some(pos) = self.latest_mouse_pos.take() {
138            events.push(Event::MouseMoved(pos));
139        }
140        if let Some(scale) = self.latest_scale_factor.take() {
141            events.push(Event::ScaleFactorChanged(scale));
142        }
143
144        // Regular events
145        events.extend(self.pending.drain(..));
146
147        self.stats.events_processed += events.len();
148        self.stats.events_dropped = self.stats.events_received - self.stats.events_processed;
149
150        EventBatch { events }
151    }
152
153    pub fn stats(&self) -> &EventStats {
154        &self.stats
155    }
156
157    pub fn reset_stats(&mut self) {
158        self.stats = EventStats::default();
159    }
160}
161
162impl Default for EventQueue {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168pub struct EventBatch {
169    events: Vec<Event>,
170}
171
172impl EventBatch {
173    pub fn iter(&self) -> impl Iterator<Item = &Event> {
174        self.events.iter()
175    }
176
177    pub fn len(&self) -> usize {
178        self.events.len()
179    }
180
181    pub fn is_empty(&self) -> bool {
182        self.events.is_empty()
183    }
184
185    pub fn dispatch<H>(&mut self, mut handler: H)
186    where
187        H: FnMut(&Event) -> HandleStatus,
188    {
189        profile_function!();
190        self.events.retain(|event| {
191            let status = handler(event);
192            !status.is_consumed()
193        });
194    }
195}
196
197#[derive(Default, Debug, Clone)]
198pub struct EventStats {
199    pub events_received: usize,
200    pub events_processed: usize,
201    pub events_dropped: usize,
202}
203
204/// System theme preference (light or dark).
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
206pub enum SystemTheme {
207    /// Light appearance.
208    Light,
209    /// Dark appearance.
210    Dark,
211}
212
213#[derive(Debug, Clone)]
214pub enum Event {
215    /// Window moved to a new physical position.
216    WindowMoved(PhysicalPosition<i32>),
217    /// Window resized to a new logical size.
218    WindowResized(LogicalSize<u32>),
219    /// Scale factor changed.
220    ScaleFactorChanged(f64),
221    /// Window focus changed.
222    Focused(bool),
223    /// Window close requested.
224    CloseRequested,
225    /// The system theme (light/dark) changed.
226    ThemeChanged(SystemTheme),
227    /// Mouse button pressed.
228    MouseButtonDown(MouseButton),
229    /// Mouse button released.
230    MouseButtonUp(MouseButton),
231    /// Mouse wheel scrolled.
232    MouseScrolled(MouseScrollDelta),
233    /// Mouse cursor moved (logical coordinates).
234    MouseMoved(LogicalPosition<f64>),
235    /// Mouse cursor entered the window.
236    MouseEntered,
237    /// Mouse cursor left the window.
238    MouseLeft,
239    /// Keyboard input event.
240    KeyInput(KeyEvent),
241    /// Touch event (touchscreen or trackpad).
242    Touch(TouchEvent),
243    /// Pinch gesture (zoom).
244    PinchGesture(PinchGesture),
245    /// Rotation gesture.
246    RotationGesture(RotationGesture),
247    /// Pan gesture (two-finger scroll).
248    PanGesture(PanGesture),
249}
250
251#[derive(Debug, Clone)]
252pub struct KeyEvent {
253    pub physical_key: PhysicalKey,
254    pub logical_key: Key,
255    pub text: Option<SmolStr>,
256    pub location: KeyLocation,
257    pub state: ElementState,
258    pub repeat: bool,
259    pub is_synthetic: bool,
260}
261
262bitflags::bitflags! {
263    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
264    pub struct HandleStatus: u8 {
265        const HANDLED = 0b00000001;
266        const CONSUMED = 0b00000010;
267    }
268}
269
270impl HandleStatus {
271    pub const fn is_consumed(&self) -> bool {
272        self.contains(Self::CONSUMED)
273    }
274
275    pub const fn is_handled(&self) -> bool {
276        self.contains(Self::HANDLED)
277    }
278
279    pub const fn consumed() -> Self {
280        Self::from_bits_truncate(Self::HANDLED.bits() | Self::CONSUMED.bits())
281    }
282
283    pub const fn handled() -> Self {
284        Self::from_bits_truncate(Self::HANDLED.bits())
285    }
286
287    pub const fn ignored() -> Self {
288        Self::empty()
289    }
290}
291
292impl Event {
293    pub(crate) fn from_winit(event: winit::event::WindowEvent, scale_factor: f64) -> Option<Self> {
294        match event {
295            WinitEvent::Moved(pos) => Some(Event::WindowMoved(pos.into())),
296            WinitEvent::Resized(size) => Some(Event::WindowResized(LogicalSize::new(
297                (size.width as f64 / scale_factor) as u32,
298                (size.height as f64 / scale_factor) as u32,
299            ))),
300            WinitEvent::ScaleFactorChanged {
301                scale_factor,
302                inner_size_writer: _,
303            } => Some(Event::ScaleFactorChanged(scale_factor)),
304            WinitEvent::Focused(focus) => Some(Event::Focused(focus)),
305            WinitEvent::CloseRequested => Some(Event::CloseRequested),
306            WinitEvent::MouseInput {
307                device_id: _,
308                state,
309                button,
310            } => match state {
311                ElementState::Pressed => Some(Event::MouseButtonDown(button)),
312                ElementState::Released => Some(Event::MouseButtonUp(button)),
313            },
314            WinitEvent::MouseWheel {
315                device_id: _,
316                delta,
317                phase: _,
318            } => Some(Event::MouseScrolled(delta)),
319            WinitEvent::CursorMoved {
320                device_id: _,
321                position,
322            } => Some(Event::MouseMoved(LogicalPosition::new(
323                position.x / scale_factor,
324                position.y / scale_factor,
325            ))),
326            WinitEvent::CursorEntered { device_id: _ } => Some(Event::MouseEntered),
327            WinitEvent::CursorLeft { device_id: _ } => Some(Event::MouseLeft),
328            WinitEvent::KeyboardInput {
329                device_id: _,
330                event,
331                is_synthetic,
332            } => Some(Event::KeyInput(KeyEvent {
333                physical_key: event.physical_key,
334                logical_key: event.logical_key,
335                location: event.location,
336                repeat: event.repeat,
337                text: event.text,
338                state: event.state,
339
340                is_synthetic,
341            })),
342            WinitEvent::Touch(touch) => {
343                let force = match touch.force {
344                    Some(winit::event::Force::Normalized(f)) => Some(f as f32),
345                    Some(winit::event::Force::Calibrated {
346                        force,
347                        max_possible_force,
348                        ..
349                    }) => Some((force / max_possible_force) as f32),
350                    None => None,
351                };
352                // DeviceId doesn't expose its inner value, so we use a hash for uniqueness
353                let device_id = {
354                    use std::hash::{Hash, Hasher};
355                    let mut hasher = std::collections::hash_map::DefaultHasher::new();
356                    touch.device_id.hash(&mut hasher);
357                    hasher.finish()
358                };
359                Some(Event::Touch(TouchEvent {
360                    device_id,
361                    id: touch.id,
362                    phase: touch.phase.into(),
363                    position: LogicalPosition::new(
364                        touch.location.x / scale_factor,
365                        touch.location.y / scale_factor,
366                    ),
367                    force,
368                }))
369            }
370            WinitEvent::PinchGesture { delta, phase, .. } => {
371                Some(Event::PinchGesture(PinchGesture {
372                    delta,
373                    phase: phase.into(),
374                }))
375            }
376            WinitEvent::RotationGesture { delta, phase, .. } => {
377                Some(Event::RotationGesture(RotationGesture {
378                    delta: delta as f64,
379                    phase: phase.into(),
380                }))
381            }
382            WinitEvent::PanGesture { delta, phase, .. } => Some(Event::PanGesture(PanGesture {
383                delta: LogicalPosition::new(
384                    delta.x as f64 / scale_factor,
385                    delta.y as f64 / scale_factor,
386                ),
387                phase: phase.into(),
388            })),
389            WinitEvent::ThemeChanged(theme) => Some(Event::ThemeChanged(match theme {
390                winit::window::Theme::Light => SystemTheme::Light,
391                winit::window::Theme::Dark => SystemTheme::Dark,
392            })),
393            // We explicitly ignore touchpad pressure (deprecated in favor of Force in Touch)
394            WinitEvent::TouchpadPressure { .. } => None,
395            // DoubleTapGesture is macOS-specific and we don't have a use case for it yet
396            WinitEvent::DoubleTapGesture { .. } => None,
397            unknown => {
398                tracing::warn!("unhandled window event: {:?}", unknown);
399                None
400            }
401        }
402    }
403}