event.rs

  1use crate::{
  2    geometry::vector::vec2f,
  3    keymap::Keystroke,
  4    platform::{Event, NavigationDirection},
  5    KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
  6    MouseMovedEvent, ScrollWheelEvent, TouchPhase,
  7};
  8use cocoa::{
  9    appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
 10    base::{id, YES},
 11    foundation::NSString as _,
 12};
 13use core_graphics::{
 14    event::{CGEvent, CGEventFlags, CGKeyCode},
 15    event_source::{CGEventSource, CGEventSourceStateID},
 16};
 17use objc::{class, msg_send, sel, sel_impl};
 18use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
 19
 20const BACKSPACE_KEY: u16 = 0x7f;
 21const SPACE_KEY: u16 = b' ' as u16;
 22const ENTER_KEY: u16 = 0x0d;
 23const NUMPAD_ENTER_KEY: u16 = 0x03;
 24const ESCAPE_KEY: u16 = 0x1b;
 25const TAB_KEY: u16 = 0x09;
 26const SHIFT_TAB_KEY: u16 = 0x19;
 27
 28pub fn key_to_native(key: &str) -> Cow<str> {
 29    use cocoa::appkit::*;
 30    let code = match key {
 31        "space" => SPACE_KEY,
 32        "backspace" => BACKSPACE_KEY,
 33        "up" => NSUpArrowFunctionKey,
 34        "down" => NSDownArrowFunctionKey,
 35        "left" => NSLeftArrowFunctionKey,
 36        "right" => NSRightArrowFunctionKey,
 37        "pageup" => NSPageUpFunctionKey,
 38        "pagedown" => NSPageDownFunctionKey,
 39        "delete" => NSDeleteFunctionKey,
 40        "f1" => NSF1FunctionKey,
 41        "f2" => NSF2FunctionKey,
 42        "f3" => NSF3FunctionKey,
 43        "f4" => NSF4FunctionKey,
 44        "f5" => NSF5FunctionKey,
 45        "f6" => NSF6FunctionKey,
 46        "f7" => NSF7FunctionKey,
 47        "f8" => NSF8FunctionKey,
 48        "f9" => NSF9FunctionKey,
 49        "f10" => NSF10FunctionKey,
 50        "f11" => NSF11FunctionKey,
 51        "f12" => NSF12FunctionKey,
 52        _ => return Cow::Borrowed(key),
 53    };
 54    Cow::Owned(String::from_utf16(&[code]).unwrap())
 55}
 56
 57impl Event {
 58    pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {
 59        let event_type = native_event.eventType();
 60
 61        // Filter out event types that aren't in the NSEventType enum.
 62        // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
 63        match event_type as u64 {
 64            0 | 21 | 32 | 33 | 35 | 36 | 37 => {
 65                return None;
 66            }
 67            _ => {}
 68        }
 69
 70        match event_type {
 71            NSEventType::NSFlagsChanged => {
 72                let modifiers = native_event.modifierFlags();
 73                let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
 74                let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
 75                let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
 76                let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
 77
 78                Some(Self::ModifiersChanged(ModifiersChangedEvent {
 79                    ctrl,
 80                    alt,
 81                    shift,
 82                    cmd,
 83                }))
 84            }
 85            NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
 86                keystroke: parse_keystroke(native_event),
 87                is_held: native_event.isARepeat() == YES,
 88            })),
 89            NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent {
 90                keystroke: parse_keystroke(native_event),
 91            })),
 92            NSEventType::NSLeftMouseDown
 93            | NSEventType::NSRightMouseDown
 94            | NSEventType::NSOtherMouseDown => {
 95                let button = match native_event.buttonNumber() {
 96                    0 => MouseButton::Left,
 97                    1 => MouseButton::Right,
 98                    2 => MouseButton::Middle,
 99                    3 => MouseButton::Navigate(NavigationDirection::Back),
100                    4 => MouseButton::Navigate(NavigationDirection::Forward),
101                    // Other mouse buttons aren't tracked currently
102                    _ => return None,
103                };
104                let modifiers = native_event.modifierFlags();
105
106                window_height.map(|window_height| {
107                    Self::MouseDown(MouseButtonEvent {
108                        button,
109                        position: vec2f(
110                            native_event.locationInWindow().x as f32,
111                            window_height - native_event.locationInWindow().y as f32,
112                        ),
113                        ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
114                        alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
115                        shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
116                        cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
117                        click_count: native_event.clickCount() as usize,
118                    })
119                })
120            }
121            NSEventType::NSLeftMouseUp
122            | NSEventType::NSRightMouseUp
123            | NSEventType::NSOtherMouseUp => {
124                let button = match native_event.buttonNumber() {
125                    0 => MouseButton::Left,
126                    1 => MouseButton::Right,
127                    2 => MouseButton::Middle,
128                    3 => MouseButton::Navigate(NavigationDirection::Back),
129                    4 => MouseButton::Navigate(NavigationDirection::Forward),
130                    // Other mouse buttons aren't tracked currently
131                    _ => return None,
132                };
133
134                window_height.map(|window_height| {
135                    let modifiers = native_event.modifierFlags();
136                    Self::MouseUp(MouseButtonEvent {
137                        button,
138                        position: vec2f(
139                            native_event.locationInWindow().x as f32,
140                            window_height - native_event.locationInWindow().y as f32,
141                        ),
142                        ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
143                        alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
144                        shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
145                        cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
146                        click_count: native_event.clickCount() as usize,
147                    })
148                })
149            }
150            NSEventType::NSScrollWheel => window_height.map(|window_height| {
151                let modifiers = native_event.modifierFlags();
152
153                let phase = match native_event.phase() {
154                    NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
155                        Some(TouchPhase::Started)
156                    }
157                    NSEventPhase::NSEventPhaseEnded => Some(TouchPhase::Ended),
158                    _ => Some(TouchPhase::Moved),
159                };
160
161                Self::ScrollWheel(ScrollWheelEvent {
162                    position: vec2f(
163                        native_event.locationInWindow().x as f32,
164                        window_height - native_event.locationInWindow().y as f32,
165                    ),
166                    delta: vec2f(
167                        native_event.scrollingDeltaX() as f32,
168                        native_event.scrollingDeltaY() as f32,
169                    ),
170                    phase,
171                    precise: native_event.hasPreciseScrollingDeltas() == YES,
172                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
173                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
174                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
175                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
176                })
177            }),
178            NSEventType::NSLeftMouseDragged
179            | NSEventType::NSRightMouseDragged
180            | NSEventType::NSOtherMouseDragged => {
181                let pressed_button = match native_event.buttonNumber() {
182                    0 => MouseButton::Left,
183                    1 => MouseButton::Right,
184                    2 => MouseButton::Middle,
185                    3 => MouseButton::Navigate(NavigationDirection::Back),
186                    4 => MouseButton::Navigate(NavigationDirection::Forward),
187                    // Other mouse buttons aren't tracked currently
188                    _ => return None,
189                };
190
191                window_height.map(|window_height| {
192                    let modifiers = native_event.modifierFlags();
193                    Self::MouseMoved(MouseMovedEvent {
194                        pressed_button: Some(pressed_button),
195                        position: vec2f(
196                            native_event.locationInWindow().x as f32,
197                            window_height - native_event.locationInWindow().y as f32,
198                        ),
199                        ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
200                        alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
201                        shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
202                        cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
203                    })
204                })
205            }
206            NSEventType::NSMouseMoved => window_height.map(|window_height| {
207                let modifiers = native_event.modifierFlags();
208                Self::MouseMoved(MouseMovedEvent {
209                    position: vec2f(
210                        native_event.locationInWindow().x as f32,
211                        window_height - native_event.locationInWindow().y as f32,
212                    ),
213                    pressed_button: None,
214                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
215                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
216                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
217                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
218                })
219            }),
220            _ => None,
221        }
222    }
223}
224
225unsafe fn parse_keystroke(native_event: id) -> Keystroke {
226    use cocoa::appkit::*;
227
228    let mut chars_ignoring_modifiers =
229        CStr::from_ptr(native_event.charactersIgnoringModifiers().UTF8String() as *mut c_char)
230            .to_str()
231            .unwrap();
232    let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16);
233    let modifiers = native_event.modifierFlags();
234
235    let ctrl = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
236    let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
237    let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
238    let cmd = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
239    let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
240        && first_char.map_or(true, |ch| {
241            !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch)
242        });
243
244    #[allow(non_upper_case_globals)]
245    let key = match first_char {
246        Some(SPACE_KEY) => "space",
247        Some(BACKSPACE_KEY) => "backspace",
248        Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => "enter",
249        Some(ESCAPE_KEY) => "escape",
250        Some(TAB_KEY) => "tab",
251        Some(SHIFT_TAB_KEY) => "tab",
252        Some(NSUpArrowFunctionKey) => "up",
253        Some(NSDownArrowFunctionKey) => "down",
254        Some(NSLeftArrowFunctionKey) => "left",
255        Some(NSRightArrowFunctionKey) => "right",
256        Some(NSPageUpFunctionKey) => "pageup",
257        Some(NSPageDownFunctionKey) => "pagedown",
258        Some(NSDeleteFunctionKey) => "delete",
259        Some(NSF1FunctionKey) => "f1",
260        Some(NSF2FunctionKey) => "f2",
261        Some(NSF3FunctionKey) => "f3",
262        Some(NSF4FunctionKey) => "f4",
263        Some(NSF5FunctionKey) => "f5",
264        Some(NSF6FunctionKey) => "f6",
265        Some(NSF7FunctionKey) => "f7",
266        Some(NSF8FunctionKey) => "f8",
267        Some(NSF9FunctionKey) => "f9",
268        Some(NSF10FunctionKey) => "f10",
269        Some(NSF11FunctionKey) => "f11",
270        Some(NSF12FunctionKey) => "f12",
271        _ => {
272            let mut chars_ignoring_modifiers_and_shift =
273                chars_for_modified_key(native_event.keyCode(), false, false);
274
275            // Honor ⌘ when Dvorak-QWERTY is used.
276            let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), true, false);
277            if cmd && chars_ignoring_modifiers_and_shift != chars_with_cmd {
278                chars_ignoring_modifiers =
279                    chars_for_modified_key(native_event.keyCode(), true, shift);
280                chars_ignoring_modifiers_and_shift = chars_with_cmd;
281            }
282
283            if shift {
284                if chars_ignoring_modifiers_and_shift
285                    == chars_ignoring_modifiers.to_ascii_lowercase()
286                {
287                    chars_ignoring_modifiers_and_shift
288                } else if chars_ignoring_modifiers_and_shift != chars_ignoring_modifiers {
289                    shift = false;
290                    chars_ignoring_modifiers
291                } else {
292                    chars_ignoring_modifiers
293                }
294            } else {
295                chars_ignoring_modifiers
296            }
297        }
298    };
299
300    Keystroke {
301        ctrl,
302        alt,
303        shift,
304        cmd,
305        function,
306        key: key.into(),
307    }
308}
309
310fn chars_for_modified_key<'a>(code: CGKeyCode, cmd: bool, shift: bool) -> &'a str {
311    // Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that
312    // always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing
313    // an event with the given flags instead lets us access `characters`, which always
314    // returns a valid string.
315    let event = CGEvent::new_keyboard_event(
316        CGEventSource::new(CGEventSourceStateID::Private).unwrap(),
317        code,
318        true,
319    )
320    .unwrap();
321    let mut flags = CGEventFlags::empty();
322    if cmd {
323        flags |= CGEventFlags::CGEventFlagCommand;
324    }
325    if shift {
326        flags |= CGEventFlags::CGEventFlagShift;
327    }
328    event.set_flags(flags);
329
330    let event: id = unsafe { msg_send![class!(NSEvent), eventWithCGEvent: event] };
331    unsafe {
332        CStr::from_ptr(event.characters().UTF8String())
333            .to_str()
334            .unwrap()
335    }
336}