events.rs

  1use crate::{
  2    Capslock, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
  3    MouseDownEvent, MouseExitEvent, MouseMoveEvent, MousePressureEvent, MouseUpEvent,
  4    NavigationDirection, Pixels, PlatformInput, PressureStage, ScrollDelta, ScrollWheelEvent,
  5    TouchPhase,
  6    platform::mac::{
  7        LMGetKbdType, NSStringExt, TISCopyCurrentKeyboardLayoutInputSource,
  8        TISGetInputSourceProperty, UCKeyTranslate, kTISPropertyUnicodeKeyLayoutData,
  9    },
 10    point, px,
 11};
 12use cocoa::{
 13    appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
 14    base::{YES, id},
 15};
 16use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
 17use core_graphics::event::CGKeyCode;
 18use objc::{msg_send, sel, sel_impl};
 19use std::{borrow::Cow, ffi::c_void};
 20
 21const BACKSPACE_KEY: u16 = 0x7f;
 22const SPACE_KEY: u16 = b' ' as u16;
 23const ENTER_KEY: u16 = 0x0d;
 24const NUMPAD_ENTER_KEY: u16 = 0x03;
 25pub(crate) const ESCAPE_KEY: u16 = 0x1b;
 26const TAB_KEY: u16 = 0x09;
 27const SHIFT_TAB_KEY: u16 = 0x19;
 28
 29pub fn key_to_native(key: &str) -> Cow<'_, str> {
 30    use cocoa::appkit::*;
 31    let code = match key {
 32        "space" => SPACE_KEY,
 33        "backspace" => BACKSPACE_KEY,
 34        "escape" => ESCAPE_KEY,
 35        "up" => NSUpArrowFunctionKey,
 36        "down" => NSDownArrowFunctionKey,
 37        "left" => NSLeftArrowFunctionKey,
 38        "right" => NSRightArrowFunctionKey,
 39        "pageup" => NSPageUpFunctionKey,
 40        "pagedown" => NSPageDownFunctionKey,
 41        "home" => NSHomeFunctionKey,
 42        "end" => NSEndFunctionKey,
 43        "delete" => NSDeleteFunctionKey,
 44        "insert" => NSHelpFunctionKey,
 45        "f1" => NSF1FunctionKey,
 46        "f2" => NSF2FunctionKey,
 47        "f3" => NSF3FunctionKey,
 48        "f4" => NSF4FunctionKey,
 49        "f5" => NSF5FunctionKey,
 50        "f6" => NSF6FunctionKey,
 51        "f7" => NSF7FunctionKey,
 52        "f8" => NSF8FunctionKey,
 53        "f9" => NSF9FunctionKey,
 54        "f10" => NSF10FunctionKey,
 55        "f11" => NSF11FunctionKey,
 56        "f12" => NSF12FunctionKey,
 57        "f13" => NSF13FunctionKey,
 58        "f14" => NSF14FunctionKey,
 59        "f15" => NSF15FunctionKey,
 60        "f16" => NSF16FunctionKey,
 61        "f17" => NSF17FunctionKey,
 62        "f18" => NSF18FunctionKey,
 63        "f19" => NSF19FunctionKey,
 64        "f20" => NSF20FunctionKey,
 65        "f21" => NSF21FunctionKey,
 66        "f22" => NSF22FunctionKey,
 67        "f23" => NSF23FunctionKey,
 68        "f24" => NSF24FunctionKey,
 69        "f25" => NSF25FunctionKey,
 70        "f26" => NSF26FunctionKey,
 71        "f27" => NSF27FunctionKey,
 72        "f28" => NSF28FunctionKey,
 73        "f29" => NSF29FunctionKey,
 74        "f30" => NSF30FunctionKey,
 75        "f31" => NSF31FunctionKey,
 76        "f32" => NSF32FunctionKey,
 77        "f33" => NSF33FunctionKey,
 78        "f34" => NSF34FunctionKey,
 79        "f35" => NSF35FunctionKey,
 80        _ => return Cow::Borrowed(key),
 81    };
 82    Cow::Owned(String::from_utf16(&[code]).unwrap())
 83}
 84
 85unsafe fn read_modifiers(native_event: id) -> Modifiers {
 86    unsafe {
 87        let modifiers = native_event.modifierFlags();
 88        let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
 89        let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
 90        let shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
 91        let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
 92        let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask);
 93
 94        Modifiers {
 95            control,
 96            alt,
 97            shift,
 98            platform: command,
 99            function,
100        }
101    }
102}
103
104impl PlatformInput {
105    pub(crate) unsafe fn from_native(
106        native_event: id,
107        window_height: Option<Pixels>,
108    ) -> Option<Self> {
109        unsafe {
110            let event_type = native_event.eventType();
111
112            // Filter out event types that aren't in the NSEventType enum.
113            // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details.
114            match event_type as u64 {
115                0 | 21 | 32 | 33 | 35 | 36 | 37 => {
116                    return None;
117                }
118                _ => {}
119            }
120
121            match event_type {
122                NSEventType::NSFlagsChanged => {
123                    Some(Self::ModifiersChanged(ModifiersChangedEvent {
124                        modifiers: read_modifiers(native_event),
125                        capslock: Capslock {
126                            on: native_event
127                                .modifierFlags()
128                                .contains(NSEventModifierFlags::NSAlphaShiftKeyMask),
129                        },
130                    }))
131                }
132                NSEventType::NSKeyDown => Some(Self::KeyDown(KeyDownEvent {
133                    keystroke: parse_keystroke(native_event),
134                    is_held: native_event.isARepeat() == YES,
135                    prefer_character_input: false,
136                })),
137                NSEventType::NSKeyUp => Some(Self::KeyUp(KeyUpEvent {
138                    keystroke: parse_keystroke(native_event),
139                })),
140                NSEventType::NSLeftMouseDown
141                | NSEventType::NSRightMouseDown
142                | NSEventType::NSOtherMouseDown => {
143                    let button = match native_event.buttonNumber() {
144                        0 => MouseButton::Left,
145                        1 => MouseButton::Right,
146                        2 => MouseButton::Middle,
147                        3 => MouseButton::Navigate(NavigationDirection::Back),
148                        4 => MouseButton::Navigate(NavigationDirection::Forward),
149                        // Other mouse buttons aren't tracked currently
150                        _ => return None,
151                    };
152                    window_height.map(|window_height| {
153                        Self::MouseDown(MouseDownEvent {
154                            button,
155                            position: point(
156                                px(native_event.locationInWindow().x as f32),
157                                // MacOS screen coordinates are relative to bottom left
158                                window_height - px(native_event.locationInWindow().y as f32),
159                            ),
160                            modifiers: read_modifiers(native_event),
161                            click_count: native_event.clickCount() as usize,
162                            first_mouse: false,
163                        })
164                    })
165                }
166                NSEventType::NSLeftMouseUp
167                | NSEventType::NSRightMouseUp
168                | NSEventType::NSOtherMouseUp => {
169                    let button = match native_event.buttonNumber() {
170                        0 => MouseButton::Left,
171                        1 => MouseButton::Right,
172                        2 => MouseButton::Middle,
173                        3 => MouseButton::Navigate(NavigationDirection::Back),
174                        4 => MouseButton::Navigate(NavigationDirection::Forward),
175                        // Other mouse buttons aren't tracked currently
176                        _ => return None,
177                    };
178
179                    window_height.map(|window_height| {
180                        Self::MouseUp(MouseUpEvent {
181                            button,
182                            position: point(
183                                px(native_event.locationInWindow().x as f32),
184                                window_height - px(native_event.locationInWindow().y as f32),
185                            ),
186                            modifiers: read_modifiers(native_event),
187                            click_count: native_event.clickCount() as usize,
188                        })
189                    })
190                }
191                NSEventType::NSEventTypePressure => {
192                    let stage = native_event.stage();
193                    let pressure = native_event.pressure();
194
195                    window_height.map(|window_height| {
196                        Self::MousePressure(MousePressureEvent {
197                            stage: match stage {
198                                1 => PressureStage::Normal,
199                                2 => PressureStage::Force,
200                                _ => PressureStage::Zero,
201                            },
202                            pressure,
203                            modifiers: read_modifiers(native_event),
204                            position: point(
205                                px(native_event.locationInWindow().x as f32),
206                                window_height - px(native_event.locationInWindow().y as f32),
207                            ),
208                        })
209                    })
210                }
211                // Some mice (like Logitech MX Master) send navigation buttons as swipe events
212                NSEventType::NSEventTypeSwipe => {
213                    let navigation_direction = match native_event.phase() {
214                        NSEventPhase::NSEventPhaseEnded => match native_event.deltaX() {
215                            x if x > 0.0 => Some(NavigationDirection::Back),
216                            x if x < 0.0 => Some(NavigationDirection::Forward),
217                            _ => return None,
218                        },
219                        _ => return None,
220                    };
221
222                    match navigation_direction {
223                        Some(direction) => window_height.map(|window_height| {
224                            Self::MouseDown(MouseDownEvent {
225                                button: MouseButton::Navigate(direction),
226                                position: point(
227                                    px(native_event.locationInWindow().x as f32),
228                                    window_height - px(native_event.locationInWindow().y as f32),
229                                ),
230                                modifiers: read_modifiers(native_event),
231                                click_count: 1,
232                                first_mouse: false,
233                            })
234                        }),
235                        _ => None,
236                    }
237                }
238                NSEventType::NSScrollWheel => window_height.map(|window_height| {
239                    let phase = match native_event.phase() {
240                        NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => {
241                            TouchPhase::Started
242                        }
243                        NSEventPhase::NSEventPhaseEnded => TouchPhase::Ended,
244                        _ => TouchPhase::Moved,
245                    };
246
247                    let raw_data = point(
248                        native_event.scrollingDeltaX() as f32,
249                        native_event.scrollingDeltaY() as f32,
250                    );
251
252                    let delta = if native_event.hasPreciseScrollingDeltas() == YES {
253                        ScrollDelta::Pixels(raw_data.map(px))
254                    } else {
255                        ScrollDelta::Lines(raw_data)
256                    };
257
258                    Self::ScrollWheel(ScrollWheelEvent {
259                        position: point(
260                            px(native_event.locationInWindow().x as f32),
261                            window_height - px(native_event.locationInWindow().y as f32),
262                        ),
263                        delta,
264                        touch_phase: phase,
265                        modifiers: read_modifiers(native_event),
266                    })
267                }),
268                NSEventType::NSLeftMouseDragged
269                | NSEventType::NSRightMouseDragged
270                | NSEventType::NSOtherMouseDragged => {
271                    let pressed_button = match native_event.buttonNumber() {
272                        0 => MouseButton::Left,
273                        1 => MouseButton::Right,
274                        2 => MouseButton::Middle,
275                        3 => MouseButton::Navigate(NavigationDirection::Back),
276                        4 => MouseButton::Navigate(NavigationDirection::Forward),
277                        // Other mouse buttons aren't tracked currently
278                        _ => return None,
279                    };
280
281                    window_height.map(|window_height| {
282                        Self::MouseMove(MouseMoveEvent {
283                            pressed_button: Some(pressed_button),
284                            position: point(
285                                px(native_event.locationInWindow().x as f32),
286                                window_height - px(native_event.locationInWindow().y as f32),
287                            ),
288                            modifiers: read_modifiers(native_event),
289                        })
290                    })
291                }
292                NSEventType::NSMouseMoved => window_height.map(|window_height| {
293                    Self::MouseMove(MouseMoveEvent {
294                        position: point(
295                            px(native_event.locationInWindow().x as f32),
296                            window_height - px(native_event.locationInWindow().y as f32),
297                        ),
298                        pressed_button: None,
299                        modifiers: read_modifiers(native_event),
300                    })
301                }),
302                NSEventType::NSMouseExited => window_height.map(|window_height| {
303                    Self::MouseExited(MouseExitEvent {
304                        position: point(
305                            px(native_event.locationInWindow().x as f32),
306                            window_height - px(native_event.locationInWindow().y as f32),
307                        ),
308
309                        pressed_button: None,
310                        modifiers: read_modifiers(native_event),
311                    })
312                }),
313                _ => None,
314            }
315        }
316    }
317}
318
319unsafe fn parse_keystroke(native_event: id) -> Keystroke {
320    unsafe {
321        use cocoa::appkit::*;
322
323        let mut characters = native_event
324            .charactersIgnoringModifiers()
325            .to_str()
326            .to_string();
327        let mut key_char = None;
328        let first_char = characters.chars().next().map(|ch| ch as u16);
329        let modifiers = native_event.modifierFlags();
330
331        let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
332        let alt = modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask);
333        let mut shift = modifiers.contains(NSEventModifierFlags::NSShiftKeyMask);
334        let command = modifiers.contains(NSEventModifierFlags::NSCommandKeyMask);
335        let function = modifiers.contains(NSEventModifierFlags::NSFunctionKeyMask)
336            && first_char
337                .is_none_or(|ch| !(NSUpArrowFunctionKey..=NSModeSwitchFunctionKey).contains(&ch));
338
339        #[allow(non_upper_case_globals)]
340        let key = match first_char {
341            Some(SPACE_KEY) => {
342                key_char = Some(" ".to_string());
343                "space".to_string()
344            }
345            Some(TAB_KEY) => {
346                key_char = Some("\t".to_string());
347                "tab".to_string()
348            }
349            Some(ENTER_KEY) | Some(NUMPAD_ENTER_KEY) => {
350                key_char = Some("\n".to_string());
351                "enter".to_string()
352            }
353            Some(BACKSPACE_KEY) => "backspace".to_string(),
354            Some(ESCAPE_KEY) => "escape".to_string(),
355            Some(SHIFT_TAB_KEY) => "tab".to_string(),
356            Some(NSUpArrowFunctionKey) => "up".to_string(),
357            Some(NSDownArrowFunctionKey) => "down".to_string(),
358            Some(NSLeftArrowFunctionKey) => "left".to_string(),
359            Some(NSRightArrowFunctionKey) => "right".to_string(),
360            Some(NSPageUpFunctionKey) => "pageup".to_string(),
361            Some(NSPageDownFunctionKey) => "pagedown".to_string(),
362            Some(NSHomeFunctionKey) => "home".to_string(),
363            Some(NSEndFunctionKey) => "end".to_string(),
364            Some(NSDeleteFunctionKey) => "delete".to_string(),
365            // Observed Insert==NSHelpFunctionKey not NSInsertFunctionKey.
366            Some(NSHelpFunctionKey) => "insert".to_string(),
367            Some(NSF1FunctionKey) => "f1".to_string(),
368            Some(NSF2FunctionKey) => "f2".to_string(),
369            Some(NSF3FunctionKey) => "f3".to_string(),
370            Some(NSF4FunctionKey) => "f4".to_string(),
371            Some(NSF5FunctionKey) => "f5".to_string(),
372            Some(NSF6FunctionKey) => "f6".to_string(),
373            Some(NSF7FunctionKey) => "f7".to_string(),
374            Some(NSF8FunctionKey) => "f8".to_string(),
375            Some(NSF9FunctionKey) => "f9".to_string(),
376            Some(NSF10FunctionKey) => "f10".to_string(),
377            Some(NSF11FunctionKey) => "f11".to_string(),
378            Some(NSF12FunctionKey) => "f12".to_string(),
379            Some(NSF13FunctionKey) => "f13".to_string(),
380            Some(NSF14FunctionKey) => "f14".to_string(),
381            Some(NSF15FunctionKey) => "f15".to_string(),
382            Some(NSF16FunctionKey) => "f16".to_string(),
383            Some(NSF17FunctionKey) => "f17".to_string(),
384            Some(NSF18FunctionKey) => "f18".to_string(),
385            Some(NSF19FunctionKey) => "f19".to_string(),
386            Some(NSF20FunctionKey) => "f20".to_string(),
387            Some(NSF21FunctionKey) => "f21".to_string(),
388            Some(NSF22FunctionKey) => "f22".to_string(),
389            Some(NSF23FunctionKey) => "f23".to_string(),
390            Some(NSF24FunctionKey) => "f24".to_string(),
391            Some(NSF25FunctionKey) => "f25".to_string(),
392            Some(NSF26FunctionKey) => "f26".to_string(),
393            Some(NSF27FunctionKey) => "f27".to_string(),
394            Some(NSF28FunctionKey) => "f28".to_string(),
395            Some(NSF29FunctionKey) => "f29".to_string(),
396            Some(NSF30FunctionKey) => "f30".to_string(),
397            Some(NSF31FunctionKey) => "f31".to_string(),
398            Some(NSF32FunctionKey) => "f32".to_string(),
399            Some(NSF33FunctionKey) => "f33".to_string(),
400            Some(NSF34FunctionKey) => "f34".to_string(),
401            Some(NSF35FunctionKey) => "f35".to_string(),
402            _ => {
403                // Cases to test when modifying this:
404                //
405                //           qwerty key | none | cmd   | cmd-shift
406                // * Armenian         s | ս    | cmd-s | cmd-shift-s  (layout is non-ASCII, so we use cmd layout)
407                // * Dvorak+QWERTY    s | o    | cmd-s | cmd-shift-s  (layout switches on cmd)
408                // * Ukrainian+QWERTY s | с    | cmd-s | cmd-shift-s  (macOS reports cmd-s instead of cmd-S)
409                // * Czech            7 | ý    | cmd-ý | cmd-7        (layout has shifted numbers)
410                // * Norwegian        7 | 7    | cmd-7 | cmd-/        (macOS reports cmd-shift-7 instead of cmd-/)
411                // * Russian          7 | 7    | cmd-7 | cmd-&        (shift-7 is . but when cmd is down, should use cmd layout)
412                // * German QWERTZ    ; | ö    | cmd-ö | cmd-Ö        (Zed's shift special case only applies to a-z)
413                //
414                let mut chars_ignoring_modifiers =
415                    chars_for_modified_key(native_event.keyCode(), NO_MOD);
416                let mut chars_with_shift =
417                    chars_for_modified_key(native_event.keyCode(), SHIFT_MOD);
418                let always_use_cmd_layout = always_use_command_layout();
419
420                // Handle Dvorak+QWERTY / Russian / Armenian
421                if command || always_use_cmd_layout {
422                    let chars_with_cmd = chars_for_modified_key(native_event.keyCode(), CMD_MOD);
423                    let chars_with_both =
424                        chars_for_modified_key(native_event.keyCode(), CMD_MOD | SHIFT_MOD);
425
426                    // We don't do this in the case that the shifted command key generates
427                    // the same character as the unshifted command key (Norwegian, e.g.)
428                    if chars_with_both != chars_with_cmd {
429                        chars_with_shift = chars_with_both;
430
431                    // Handle edge-case where cmd-shift-s reports cmd-s instead of
432                    // cmd-shift-s (Ukrainian, etc.)
433                    } else if chars_with_cmd.to_ascii_uppercase() != chars_with_cmd {
434                        chars_with_shift = chars_with_cmd.to_ascii_uppercase();
435                    }
436                    chars_ignoring_modifiers = chars_with_cmd;
437                }
438
439                if !control && !command && !function {
440                    let mut mods = NO_MOD;
441                    if shift {
442                        mods |= SHIFT_MOD;
443                    }
444                    if alt {
445                        mods |= OPTION_MOD;
446                    }
447
448                    key_char = Some(chars_for_modified_key(native_event.keyCode(), mods));
449                }
450
451                if shift
452                    && chars_ignoring_modifiers
453                        .chars()
454                        .all(|c| c.is_ascii_lowercase())
455                {
456                    chars_ignoring_modifiers
457                } else if shift {
458                    shift = false;
459                    chars_with_shift
460                } else {
461                    chars_ignoring_modifiers
462                }
463            }
464        };
465
466        Keystroke {
467            modifiers: Modifiers {
468                control,
469                alt,
470                shift,
471                platform: command,
472                function,
473            },
474            key,
475            key_char,
476        }
477    }
478}
479
480fn always_use_command_layout() -> bool {
481    if chars_for_modified_key(0, NO_MOD).is_ascii() {
482        return false;
483    }
484
485    chars_for_modified_key(0, CMD_MOD).is_ascii()
486}
487
488const NO_MOD: u32 = 0;
489const CMD_MOD: u32 = 1;
490const SHIFT_MOD: u32 = 2;
491const OPTION_MOD: u32 = 8;
492
493fn chars_for_modified_key(code: CGKeyCode, modifiers: u32) -> String {
494    // Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
495    // shifted >> 8 for UCKeyTranslate
496    const CG_SPACE_KEY: u16 = 49;
497    // https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
498    #[allow(non_upper_case_globals)]
499    const kUCKeyActionDown: u16 = 0;
500    #[allow(non_upper_case_globals)]
501    const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
502
503    let keyboard_type = unsafe { LMGetKbdType() as u32 };
504    const BUFFER_SIZE: usize = 4;
505    let mut dead_key_state = 0;
506    let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
507    let mut buffer_size: usize = 0;
508
509    let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
510    if keyboard.is_null() {
511        return "".to_string();
512    }
513    let layout_data = unsafe {
514        TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
515            as CFDataRef
516    };
517    if layout_data.is_null() {
518        unsafe {
519            let _: () = msg_send![keyboard, release];
520        }
521        return "".to_string();
522    }
523    let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
524
525    unsafe {
526        UCKeyTranslate(
527            keyboard_layout as *const c_void,
528            code,
529            kUCKeyActionDown,
530            modifiers,
531            keyboard_type,
532            kUCKeyTranslateNoDeadKeysMask,
533            &mut dead_key_state,
534            BUFFER_SIZE,
535            &mut buffer_size as *mut usize,
536            &mut buffer as *mut u16,
537        );
538        if dead_key_state != 0 {
539            UCKeyTranslate(
540                keyboard_layout as *const c_void,
541                CG_SPACE_KEY,
542                kUCKeyActionDown,
543                modifiers,
544                keyboard_type,
545                kUCKeyTranslateNoDeadKeysMask,
546                &mut dead_key_state,
547                BUFFER_SIZE,
548                &mut buffer_size as *mut usize,
549                &mut buffer as *mut u16,
550            );
551        }
552        let _: () = msg_send![keyboard, release];
553    }
554    String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
555}