Skip to main content

telar_platform_winit/
map.rs

1use platform_core::{Event, PointerButton, PointerSource, ScrollDelta};
2use winit::event::{ElementState, MouseScrollDelta, Touch, TouchPhase, WindowEvent};
3use winit::event::{Modifiers, MouseButton as WinitMouseButton};
4use winit::keyboard::{Key as WinitKey, KeyLocation, NamedKey as WinitNamedKey};
5
6/// Translates a winit logical key into a [`platform_core::Key`], resolving the keypad from `location`.
7///
8/// Both backends go through here so a key can never reach one and not the other. `location` is what
9/// separates the keypad from the digit row: winit reports `Numpad7` as the character `"7"` and only the
10/// location says which key was struck.
11pub fn map_key(key: &WinitKey, location: KeyLocation) -> Option<platform_core::Key> {
12    if location == KeyLocation::Numpad
13        && let Some(nk) = map_numpad(key)
14    {
15        return Some(platform_core::Key::Named(nk));
16    }
17    match key {
18        WinitKey::Character(c) => c.as_str().chars().next().map(platform_core::Key::Char),
19        WinitKey::Named(named) => map_named_key(*named).map(platform_core::Key::Named),
20        _ => None,
21    }
22}
23
24fn map_numpad(key: &WinitKey) -> Option<platform_core::NamedKey> {
25    use platform_core::NamedKey as Nk;
26    let nk = match key {
27        WinitKey::Named(WinitNamedKey::Enter) => Nk::NumpadEnter,
28        WinitKey::Character(c) => match c.as_str() {
29            "0" => Nk::Numpad0,
30            "1" => Nk::Numpad1,
31            "2" => Nk::Numpad2,
32            "3" => Nk::Numpad3,
33            "4" => Nk::Numpad4,
34            "5" => Nk::Numpad5,
35            "6" => Nk::Numpad6,
36            "7" => Nk::Numpad7,
37            "8" => Nk::Numpad8,
38            "9" => Nk::Numpad9,
39            "+" => Nk::NumpadAdd,
40            "-" => Nk::NumpadSubtract,
41            "*" => Nk::NumpadMultiply,
42            "/" => Nk::NumpadDivide,
43            // The separator is a comma on a keyboard whose locale writes decimals that way.
44            "." | "," => Nk::NumpadDecimal,
45            _ => return None,
46        },
47        _ => return None,
48    };
49    Some(nk)
50}
51
52// Shared winit->platform_core translation used by every winit backend (desktop + android). Keeping the
53// NamedKey table in one place avoids the two-backend hazard where a new key is added to one match only.
54pub fn map_named_key(key: WinitNamedKey) -> Option<platform_core::NamedKey> {
55    let nk = match key {
56        WinitNamedKey::Enter => platform_core::NamedKey::Enter,
57        WinitNamedKey::Backspace => platform_core::NamedKey::Backspace,
58        WinitNamedKey::Escape => platform_core::NamedKey::Escape,
59        WinitNamedKey::Tab => platform_core::NamedKey::Tab,
60        WinitNamedKey::Delete => platform_core::NamedKey::Delete,
61        WinitNamedKey::Home => platform_core::NamedKey::Home,
62        WinitNamedKey::End => platform_core::NamedKey::End,
63        WinitNamedKey::PageUp => platform_core::NamedKey::PageUp,
64        WinitNamedKey::PageDown => platform_core::NamedKey::PageDown,
65        WinitNamedKey::ArrowUp => platform_core::NamedKey::ArrowUp,
66        WinitNamedKey::ArrowDown => platform_core::NamedKey::ArrowDown,
67        WinitNamedKey::ArrowLeft => platform_core::NamedKey::ArrowLeft,
68        WinitNamedKey::ArrowRight => platform_core::NamedKey::ArrowRight,
69        WinitNamedKey::F1 => platform_core::NamedKey::F1,
70        WinitNamedKey::F2 => platform_core::NamedKey::F2,
71        WinitNamedKey::F3 => platform_core::NamedKey::F3,
72        WinitNamedKey::F4 => platform_core::NamedKey::F4,
73        WinitNamedKey::F5 => platform_core::NamedKey::F5,
74        WinitNamedKey::F6 => platform_core::NamedKey::F6,
75        WinitNamedKey::F7 => platform_core::NamedKey::F7,
76        WinitNamedKey::F8 => platform_core::NamedKey::F8,
77        WinitNamedKey::F9 => platform_core::NamedKey::F9,
78        WinitNamedKey::F10 => platform_core::NamedKey::F10,
79        WinitNamedKey::F11 => platform_core::NamedKey::F11,
80        WinitNamedKey::F12 => platform_core::NamedKey::F12,
81        WinitNamedKey::F13 => platform_core::NamedKey::F13,
82        WinitNamedKey::F14 => platform_core::NamedKey::F14,
83        WinitNamedKey::F15 => platform_core::NamedKey::F15,
84        WinitNamedKey::F16 => platform_core::NamedKey::F16,
85        WinitNamedKey::F17 => platform_core::NamedKey::F17,
86        WinitNamedKey::F18 => platform_core::NamedKey::F18,
87        WinitNamedKey::F19 => platform_core::NamedKey::F19,
88        WinitNamedKey::F20 => platform_core::NamedKey::F20,
89        WinitNamedKey::F21 => platform_core::NamedKey::F21,
90        WinitNamedKey::F22 => platform_core::NamedKey::F22,
91        WinitNamedKey::F23 => platform_core::NamedKey::F23,
92        WinitNamedKey::F24 => platform_core::NamedKey::F24,
93        WinitNamedKey::Space => platform_core::NamedKey::Space,
94        WinitNamedKey::Insert => platform_core::NamedKey::Insert,
95        WinitNamedKey::CapsLock => platform_core::NamedKey::CapsLock,
96        _ => return None,
97    };
98    Some(nk)
99}
100
101pub fn map_mouse_button(button: WinitMouseButton) -> Option<platform_core::PointerButton> {
102    match button {
103        WinitMouseButton::Left => Some(platform_core::PointerButton::Primary),
104        WinitMouseButton::Right => Some(platform_core::PointerButton::Secondary),
105        WinitMouseButton::Middle => Some(platform_core::PointerButton::Auxiliary),
106        _ => None,
107    }
108}
109
110pub fn map_modifiers(mods: &Modifiers) -> platform_core::ModifiersState {
111    platform_core::ModifiersState {
112        is_shift: mods.state().shift_key(),
113        is_ctrl: mods.state().control_key(),
114        is_alt: mods.state().alt_key(),
115        is_meta: mods.state().super_key(),
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use platform_core::{Key, NamedKey};
123    use winit::keyboard::SmolStr;
124
125    fn character(s: &str) -> WinitKey {
126        WinitKey::Character(SmolStr::new(s))
127    }
128
129    /// The keypad and the digit row send the same logical key; only the location tells them apart. An
130    /// application that binds Numpad 7 to a view means that key and not the 7 above the letters.
131    #[test]
132    fn the_keypad_is_its_own_set_of_keys() {
133        assert_eq!(
134            map_key(&character("7"), KeyLocation::Numpad),
135            Some(Key::Named(NamedKey::Numpad7))
136        );
137        assert_eq!(
138            map_key(&character("7"), KeyLocation::Standard),
139            Some(Key::Char('7'))
140        );
141        assert_eq!(
142            map_key(&WinitKey::Named(WinitNamedKey::Enter), KeyLocation::Numpad),
143            Some(Key::Named(NamedKey::NumpadEnter))
144        );
145    }
146
147    /// With Num Lock off the OS says the keypad's 1 is `End`, and that is what it reports: overriding it
148    /// would take the arrows away from someone navigating with the keypad.
149    #[test]
150    fn a_keypad_key_without_num_lock_stays_what_the_os_calls_it() {
151        assert_eq!(
152            map_key(&WinitKey::Named(WinitNamedKey::End), KeyLocation::Numpad),
153            Some(Key::Named(NamedKey::End))
154        );
155    }
156}
157
158// What a mapped winit `WindowEvent` means at the platform level, decoupled from *how* it's applied. The
159// single-window runner applies it to a handler directly; the multi-window runner forwards it to that
160// surface's worker thread. Keeping the mapping here (and the application at the call site) lets both share
161// the exact same winit→platform translation.
162pub enum SurfaceIntent {
163    // Deliver this platform event to the handler.
164    Event(Event),
165    // Deliver this platform event, then request a redraw (winit `Resized`).
166    Resized(Event),
167    // Render now (winit `RedrawRequested`).
168    Redraw,
169    // Deliver `WindowCloseRequested`, then close this surface.
170    Close(Event),
171    // State-only (e.g. `ModifiersChanged`) or an unmapped event — nothing to deliver.
172    Ignore,
173}
174
175// Pure winit `WindowEvent` → [`SurfaceIntent`] translation, updating this surface's cursor/scale/modifiers.
176// No handler and no window side effects, so it can run on the winit thread while the handler lives elsewhere.
177pub fn map_window_event(
178    event: WindowEvent,
179    cursor_position: &mut (f64, f64),
180    scale_factor: &mut f64,
181    modifiers: &mut platform_core::ModifiersState,
182) -> SurfaceIntent {
183    match event {
184        WindowEvent::CloseRequested => SurfaceIntent::Close(Event::WindowCloseRequested),
185        WindowEvent::Resized(size) => SurfaceIntent::Resized(Event::WindowResized {
186            width: (size.width as f64 / *scale_factor).round() as u32,
187            height: (size.height as f64 / *scale_factor).round() as u32,
188        }),
189        WindowEvent::RedrawRequested => SurfaceIntent::Redraw,
190        WindowEvent::CursorMoved { position, .. } => {
191            let lx = position.x / *scale_factor;
192            let ly = position.y / *scale_factor;
193            *cursor_position = (lx, ly);
194            SurfaceIntent::Event(Event::PointerMoved {
195                x: lx,
196                y: ly,
197                source: PointerSource::Mouse,
198            })
199        }
200        WindowEvent::MouseInput { state, button, .. } => {
201            let Some(btn) = crate::map_mouse_button(button) else {
202                return SurfaceIntent::Ignore;
203            };
204            let (x, y) = *cursor_position;
205            SurfaceIntent::Event(match state {
206                ElementState::Pressed => Event::PointerPressed {
207                    x,
208                    y,
209                    button: btn,
210                    source: PointerSource::Mouse,
211                },
212                ElementState::Released => Event::PointerReleased {
213                    x,
214                    y,
215                    button: btn,
216                    source: PointerSource::Mouse,
217                },
218            })
219        }
220        WindowEvent::Touch(Touch {
221            phase,
222            location,
223            id,
224            ..
225        }) => {
226            let x = location.x / *scale_factor;
227            let y = location.y / *scale_factor;
228            let source = PointerSource::Touch { id };
229            SurfaceIntent::Event(match phase {
230                TouchPhase::Started => Event::PointerPressed {
231                    x,
232                    y,
233                    button: PointerButton::Primary,
234                    source,
235                },
236                TouchPhase::Moved => Event::PointerMoved { x, y, source },
237                TouchPhase::Ended | TouchPhase::Cancelled => Event::PointerReleased {
238                    x,
239                    y,
240                    button: PointerButton::Primary,
241                    source,
242                },
243            })
244        }
245        WindowEvent::Focused(is_focused) => {
246            SurfaceIntent::Event(Event::FocusChanged { is_focused })
247        }
248        WindowEvent::CursorEntered { .. } => SurfaceIntent::Event(Event::CursorEntered),
249        WindowEvent::CursorLeft { .. } => SurfaceIntent::Event(Event::CursorLeft),
250        WindowEvent::ScaleFactorChanged {
251            scale_factor: new_scale,
252            ..
253        } => {
254            *scale_factor = new_scale;
255            SurfaceIntent::Event(Event::ScaleFactorChanged {
256                scale_factor: new_scale,
257            })
258        }
259        WindowEvent::MouseWheel { delta, .. } => {
260            let scroll_delta = match delta {
261                MouseScrollDelta::LineDelta(x, y) => ScrollDelta::Lines { x, y },
262                MouseScrollDelta::PixelDelta(pos) => ScrollDelta::Pixels {
263                    x: (pos.x / *scale_factor) as f32,
264                    y: (pos.y / *scale_factor) as f32,
265                },
266            };
267            let (x, y) = *cursor_position;
268            SurfaceIntent::Event(Event::Scrolled {
269                delta: scroll_delta,
270                x,
271                y,
272            })
273        }
274        WindowEvent::ModifiersChanged(mods) => {
275            *modifiers = crate::map_modifiers(&mods);
276            SurfaceIntent::Event(Event::ModifiersChanged {
277                modifiers: *modifiers,
278            })
279        }
280        WindowEvent::KeyboardInput { event, .. } => {
281            let Some(key) = crate::map_key(&event.logical_key, event.location) else {
282                return SurfaceIntent::Ignore;
283            };
284            let mods = *modifiers;
285            SurfaceIntent::Event(match event.state {
286                ElementState::Pressed => Event::KeyPressed {
287                    key,
288                    modifiers: mods,
289                },
290                ElementState::Released => Event::KeyReleased {
291                    key,
292                    modifiers: mods,
293                },
294            })
295        }
296        WindowEvent::ThemeChanged(theme) => SurfaceIntent::Event(Event::ColorSchemeChanged {
297            dark: theme == winit::window::Theme::Dark,
298        }),
299        _ => SurfaceIntent::Ignore,
300    }
301}