From f000b82d74c9288cdb0e8f37868c539046b63c1f Mon Sep 17 00:00:00 2001
From: Michael Palmos <toqoz@hotmail.com>
Date: Sun, 24 Feb 2019 06:41:55 +1000
Subject: [PATCH] Fix incorrect keycodes when using a non-US keyboard layout.
 (#755)

* Fix incorrect keycodes when using a non-US keyboard layout.

This commit fixes the issue described in #752, and uses the advised
method to fix it.

* Style fixes

Co-Authored-By: Toqozz <toqoz@hotmail.com>

* Refactoring of macOS `virtualkeycode` fix (#752)

* Applies requested changes as per pull request discussion (#755).
---
 CHANGELOG.md                          |   1 +
 src/platform_impl/macos/event_loop.rs | 103 +++++++++++++++++++++-----
 src/platform_impl/macos/view.rs       |  85 ++++++++++++---------
 3 files changed, 137 insertions(+), 52 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c8bfd68..3979c0cc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@
     - `LoopDestroyed` is emitted when the `run` or `run_return` method is about to exit.
 - Rename `MonitorId` to `MonitorHandle`.
 - Removed `serde` implementations from `ControlFlow`.
+- On macOS, fix keycodes being incorrect when using a non-US keyboard layout.
 - On Wayland, fix `with_title()` not setting the windows title
 - On Wayland, add `set_wayland_theme()` to control client decoration color theme
 - Added serde serialization to `os::unix::XWindowType`.
diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs
index d5da8faf..f7a1642e 100644
--- a/src/platform_impl/macos/event_loop.rs
+++ b/src/platform_impl/macos/event_loop.rs
@@ -544,7 +544,67 @@ impl Proxy {
     }
 }
 
-pub fn to_virtual_key_code(code: c_ushort) -> Option<events::VirtualKeyCode> {
+pub fn char_to_keycode(c: char) -> Option<events::VirtualKeyCode> {
+    // We only translate keys that are affected by keyboard layout.
+    //
+    // Note that since keys are translated in a somewhat "dumb" way (reading character)
+    // there is a concern that some combination, i.e. Cmd+char, causes the wrong
+    // letter to be received, and so we receive the wrong key.
+    //
+    // Implementation reference: https://github.com/WebKit/webkit/blob/82bae82cf0f329dbe21059ef0986c4e92fea4ba6/Source/WebCore/platform/cocoa/KeyEventCocoa.mm#L626
+    Some(match c {
+        'a' | 'A' => events::VirtualKeyCode::A,
+        'b' | 'B' => events::VirtualKeyCode::B,
+        'c' | 'C' => events::VirtualKeyCode::C,
+        'd' | 'D' => events::VirtualKeyCode::D,
+        'e' | 'E' => events::VirtualKeyCode::E,
+        'f' | 'F' => events::VirtualKeyCode::F,
+        'g' | 'G' => events::VirtualKeyCode::G,
+        'h' | 'H' => events::VirtualKeyCode::H,
+        'i' | 'I' => events::VirtualKeyCode::I,
+        'j' | 'J' => events::VirtualKeyCode::J,
+        'k' | 'K' => events::VirtualKeyCode::K,
+        'l' | 'L' => events::VirtualKeyCode::L,
+        'm' | 'M' => events::VirtualKeyCode::M,
+        'n' | 'N' => events::VirtualKeyCode::N,
+        'o' | 'O' => events::VirtualKeyCode::O,
+        'p' | 'P' => events::VirtualKeyCode::P,
+        'q' | 'Q' => events::VirtualKeyCode::Q,
+        'r' | 'R' => events::VirtualKeyCode::R,
+        's' | 'S' => events::VirtualKeyCode::S,
+        't' | 'T' => events::VirtualKeyCode::T,
+        'u' | 'U' => events::VirtualKeyCode::U,
+        'v' | 'V' => events::VirtualKeyCode::V,
+        'w' | 'W' => events::VirtualKeyCode::W,
+        'x' | 'X' => events::VirtualKeyCode::X,
+        'y' | 'Y' => events::VirtualKeyCode::Y,
+        'z' | 'Z' => events::VirtualKeyCode::Z,
+        '1' | '!' => events::VirtualKeyCode::Key1,
+        '2' | '@' => events::VirtualKeyCode::Key2,
+        '3' | '#' => events::VirtualKeyCode::Key3,
+        '4' | '$' => events::VirtualKeyCode::Key4,
+        '5' | '%' => events::VirtualKeyCode::Key5,
+        '6' | '^' => events::VirtualKeyCode::Key6,
+        '7' | '&' => events::VirtualKeyCode::Key7,
+        '8' | '*' => events::VirtualKeyCode::Key8,
+        '9' | '(' => events::VirtualKeyCode::Key9,
+        '0' | ')' => events::VirtualKeyCode::Key0,
+        '=' | '+' => events::VirtualKeyCode::Equals,
+        '-' | '_' => events::VirtualKeyCode::Minus,
+        ']' | '}' => events::VirtualKeyCode::RBracket,
+        '[' | '{' => events::VirtualKeyCode::LBracket,
+        '\''| '"' => events::VirtualKeyCode::Apostrophe,
+        ';' | ':' => events::VirtualKeyCode::Semicolon,
+        '\\'| '|' => events::VirtualKeyCode::Backslash,
+        ',' | '<' => events::VirtualKeyCode::Comma,
+        '/' | '?' => events::VirtualKeyCode::Slash,
+        '.' | '>' => events::VirtualKeyCode::Period,
+        '`' | '~' => events::VirtualKeyCode::Grave,
+        _ => return None,
+    })
+}
+
+pub fn scancode_to_keycode(code: c_ushort) -> Option<events::VirtualKeyCode> {
     Some(match code {
         0x00 => events::VirtualKeyCode::A,
         0x01 => events::VirtualKeyCode::S,
@@ -680,20 +740,19 @@ pub fn to_virtual_key_code(code: c_ushort) -> Option<events::VirtualKeyCode> {
     })
 }
 
-pub fn check_additional_virtual_key_codes(
-    s: &Option<String>
+pub fn check_function_keys(
+    s: &String
 ) -> Option<events::VirtualKeyCode> {
-    if let &Some(ref s) = s {
-        if let Some(ch) = s.encode_utf16().next() {
-            return Some(match ch {
-                0xf718 => events::VirtualKeyCode::F21,
-                0xf719 => events::VirtualKeyCode::F22,
-                0xf71a => events::VirtualKeyCode::F23,
-                0xf71b => events::VirtualKeyCode::F24,
-                _ => return None,
-            })
-        }
+    if let Some(ch) = s.encode_utf16().next() {
+        return Some(match ch {
+            0xf718 => events::VirtualKeyCode::F21,
+            0xf719 => events::VirtualKeyCode::F22,
+            0xf71a => events::VirtualKeyCode::F23,
+            0xf71b => events::VirtualKeyCode::F24,
+            _ => return None,
+        })
     }
+
     None
 }
 
@@ -709,6 +768,16 @@ pub fn event_mods(event: cocoa::base::id) -> ModifiersState {
     }
 }
 
+pub fn get_scancode(event: cocoa::base::id) -> c_ushort {
+    // In AppKit, `keyCode` refers to the position (scancode) of a key rather than its character,
+    // and there is no easy way to navtively retrieve the layout-dependent character.
+    // In winit, we use keycode to refer to the key's character, and so this function aligns
+    // AppKit's terminology with ours.
+    unsafe {
+        msg_send![event, keyCode]
+    }
+}
+
 unsafe fn modifier_event(
     ns_event: cocoa::base::id,
     keymask: NSEventModifierFlags,
@@ -721,14 +790,14 @@ unsafe fn modifier_event(
         } else {
             ElementState::Pressed
         };
-        let keycode = NSEvent::keyCode(ns_event);
-        let scancode = keycode as u32;
-        let virtual_keycode = to_virtual_key_code(keycode);
+
+        let scancode = get_scancode(ns_event);
+        let virtual_keycode = scancode_to_keycode(scancode);
         Some(WindowEvent::KeyboardInput {
             device_id: DEVICE_ID,
             input: KeyboardInput {
                 state,
-                scancode,
+                scancode: scancode as u32,
                 virtual_keycode,
                 modifiers: event_mods(ns_event),
             },
diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs
index 757982c5..d6e73331 100644
--- a/src/platform_impl/macos/view.rs
+++ b/src/platform_impl/macos/view.rs
@@ -14,10 +14,11 @@ use objc::declare::ClassDecl;
 use objc::runtime::{Class, Object, Protocol, Sel, BOOL, YES};
 
 use {ElementState, Event, KeyboardInput, MouseButton, WindowEvent, WindowId};
-use platform_impl::platform::event_loop::{DEVICE_ID, event_mods, Shared, to_virtual_key_code, check_additional_virtual_key_codes};
+use platform_impl::platform::event_loop::{DEVICE_ID, event_mods, Shared, to_virtual_key_code, check_additional_virtual_key_codes, get_scancode};
 use platform_impl::platform::util;
 use platform_impl::platform::ffi::*;
 use platform_impl::platform::window::{get_window_id, IdRef};
+use event;
 
 struct ViewState {
     window: id,
@@ -391,36 +392,68 @@ extern fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) {
     }
 }
 
-fn get_characters(event: id) -> Option<String> {
+fn get_characters(event: id, ignore_modifiers: bool) -> String {
     unsafe {
-        let characters: id = msg_send![event, characters];
+        let characters: id = if ignore_modifiers {
+            msg_send![event, charactersIgnoringModifiers]
+        } else {
+            msg_send![event, characters]
+        };
+
+        assert_ne!(characters, nil);
         let slice = slice::from_raw_parts(
             characters.UTF8String() as *const c_uchar,
             characters.len(),
         );
+
         let string = str::from_utf8_unchecked(slice);
-        Some(string.to_owned())
+
+        string.to_owned()
     }
 }
 
+// Retrieves a layout-independent keycode given an event.
+fn retrieve_keycode(event: id) -> Option<event::VirtualKeyCode> {
+    #[inline]
+    fn get_code(ev: id, raw: bool) -> Option<event::VirtualKeyCode> {
+        let characters = get_characters(ev, raw);
+        characters.chars().next().map_or(None, |c| char_to_keycode(c))
+    }
+
+    // Cmd switches Roman letters for Dvorak-QWERTY layout, so we try modified characters first.
+    // If we don't get a match, then we fall back to unmodified characters.
+    let code = get_code(event, false)
+        .or_else(|| {
+            get_code(event, true)
+        });
+
+    // We've checked all layout related keys, so fall through to scancode.
+    // Reaching this code means that the key is layout-independent (e.g. Backspace, Return).
+    //
+    // We're additionally checking here for F21-F24 keys, since their keycode
+    // can vary, but we know that they are encoded
+    // in characters property.
+    code.or_else(|| {
+        let scancode = get_scancode(event);
+        scancode_to_keycode(scancode)
+            .or_else(|| {
+                check_function_keys(&get_characters(event, true))
+            })
+    })
+}
+
 extern fn key_down(this: &Object, _sel: Sel, event: id) {
     //println!("keyDown");
     unsafe {
         let state_ptr: *mut c_void = *this.get_ivar("winitState");
         let state = &mut *(state_ptr as *mut ViewState);
         let window_id = WindowId(get_window_id(state.window));
+        let characters = get_characters(event, false);
 
-        state.raw_characters = get_characters(event);
+        state.raw_characters = Some(characters.clone());
 
-        let keycode: c_ushort = msg_send![event, keyCode];
-        // We are checking here for F21-F24 keys, since their keycode
-        // can vary, but we know that they are encoded
-        // in characters property.
-        let virtual_keycode = to_virtual_key_code(keycode)
-            .or_else(|| {
-                check_additional_virtual_key_codes(&state.raw_characters)
-            });
-        let scancode = keycode as u32;
+        let scancode = get_scancode(event) as u32;
+        let virtual_keycode = retrieve_keycode(event);
         let is_repeat = msg_send![event, isARepeat];
 
         let window_event = Event::WindowEvent {
@@ -436,17 +469,6 @@ extern fn key_down(this: &Object, _sel: Sel, event: id) {
             },
         };
 
-        let characters: id = msg_send![event, characters];
-        let slice = slice::from_raw_parts(
-            characters.UTF8String() as *const c_uchar,
-            characters.len(),
-            );
-        let string = str::from_utf8_unchecked(slice);
-
-        state.raw_characters = {
-            Some(string.to_owned())
-        };
-
         if let Some(shared) = state.shared.upgrade() {
             shared.pending_events
                 .lock()
@@ -454,7 +476,7 @@ extern fn key_down(this: &Object, _sel: Sel, event: id) {
                 .push_back(window_event);
             // Emit `ReceivedCharacter` for key repeats
             if is_repeat && state.is_key_down{
-                for character in string.chars() {
+                for character in characters.chars() {
                     let window_event = Event::WindowEvent {
                         window_id,
                         event: WindowEvent::ReceivedCharacter(character),
@@ -483,16 +505,9 @@ extern fn key_up(this: &Object, _sel: Sel, event: id) {
 
         state.is_key_down = false;
 
-        // We need characters here to check for additional keys such as
-        // F21-F24.
-        let characters = get_characters(event);
+        let scancode = get_scancode(event) as u32;
+        let virtual_keycode = retrieve_keycode(event);
 
-        let keycode: c_ushort = msg_send![event, keyCode];
-        let virtual_keycode = to_virtual_key_code(keycode)
-            .or_else(|| {
-                check_additional_virtual_key_codes(&characters)
-            });
-        let scancode = keycode as u32;
         let window_event = Event::WindowEvent {
             window_id: WindowId(get_window_id(state.window)),
             event: WindowEvent::KeyboardInput {