From 9748e16ebe9346dd6830c24599308d7f912bf1d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Frosteg=C3=A5rd?= Date: Fri, 13 Nov 2020 20:41:17 +0100 Subject: [PATCH] Add support for macOS and X11 key events with code from druid --- Cargo.toml | 1 + src/event.rs | 10 +- src/keyboard.rs | 274 +++++++++++++++++++++++++-- src/lib.rs | 2 +- src/macos/keyboard.rs | 367 +++++++++++++++++++++++++++++++++++ src/macos/mod.rs | 1 + src/macos/view.rs | 39 +++- src/macos/window.rs | 14 +- src/x11/keyboard.rs | 431 ++++++++++++++++++++++++++++++++++++++++++ src/x11/mod.rs | 3 +- src/x11/window.rs | 19 +- 11 files changed, 1124 insertions(+), 37 deletions(-) create mode 100644 src/macos/keyboard.rs create mode 100644 src/x11/keyboard.rs diff --git a/Cargo.toml b/Cargo.toml index 31b76e7..8f4c5d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ license = "MIT OR Apache-2.0" [dependencies] log = "0.4.11" +keyboard-types = "0.5.0" raw-window-handle = "0.3.3" [target.'cfg(target_os="linux")'.dependencies] diff --git a/src/event.rs b/src/event.rs index 6fae242..23face7 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,12 +1,12 @@ -use crate::{WindowInfo, Point}; +use crate::{WindowInfo, Point, KeyEvent}; -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum KeyboardEvent { - KeyPressed(u32), - KeyReleased(u32), - CharacterInput(char), + KeyPressed(KeyEvent), + KeyReleased(KeyEvent), } + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum MouseButton { Left, diff --git a/src/keyboard.rs b/src/keyboard.rs index 796bf7e..796e180 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -1,26 +1,260 @@ -// TODO: Add a method to the Window that returns the -// current modifier state. +// Copyright 2020 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -/// The current state of the keyboard modifiers. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct ModifiersState { - pub shift: bool, - pub control: bool, - pub alt: bool, - pub logo: bool, +// Baseview modifications to druid code: +// - move code_to_location function to this file + +//! Keyboard types. + +// This is a reasonable lint, but we keep signatures in sync with the +// bitflags implementation of the inner Modifiers type. +#![allow(clippy::trivially_copy_pass_by_ref)] + +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; + +pub use keyboard_types::{Code, KeyState, Location}; + +/// The meaning (mapped value) of a keypress. +pub type KbKey = keyboard_types::Key; + +/// Information about a keyboard event. +/// +/// Note that this type is similar to [`KeyboardEvent`] in keyboard-types, +/// but has a few small differences for convenience. It is missing the `state` +/// field because that is already implicit in the event. +/// +/// [`KeyboardEvent`]: keyboard_types::KeyboardEvent +#[non_exhaustive] +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct KeyEvent { + /// Whether the key is pressed or released. + pub state: KeyState, + /// Logical key value. + pub key: KbKey, + /// Physical key position. + pub code: Code, + /// Location for keys with multiple instances on common keyboards. + pub location: Location, + /// Flags for pressed modifier keys. + pub mods: Modifiers, + /// True if the key is currently auto-repeated. + pub repeat: bool, + /// Events with this flag should be ignored in a text editor + /// and instead composition events should be used. + pub is_composing: bool, } -impl ModifiersState { - /// Returns true if the current [`ModifiersState`] has at least the same - /// modifiers enabled as the given value, and false otherwise. - /// - /// [`ModifiersState`]: struct.ModifiersState.html - pub fn matches_atleast(&self, modifiers: ModifiersState) -> bool { - let shift = !modifiers.shift || self.shift; - let control = !modifiers.control || self.control; - let alt = !modifiers.alt || self.alt; - let logo = !modifiers.logo || self.logo; +/// The modifiers. +/// +/// This type is a thin wrappers around [`keyboard_types::Modifiers`], +/// mostly for the convenience methods. If those get upstreamed, it +/// will simply become that type. +/// +/// [`keyboard_types::Modifiers`]: keyboard_types::Modifiers +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub struct Modifiers(keyboard_types::Modifiers); - shift && control && alt && logo +/// A convenience trait for creating Key objects. +/// +/// This trait is implemented by [`KbKey`] itself and also strings, which are +/// converted into the `Character` variant. It is defined this way and not +/// using the standard `Into` mechanism because `KbKey` is a type in an external +/// crate. +/// +/// [`KbKey`]: KbKey +pub trait IntoKey { + fn into_key(self) -> KbKey; +} + +impl KeyEvent { + #[doc(hidden)] + /// Create a key event for testing purposes. + pub fn for_test(mods: impl Into, key: impl IntoKey) -> KeyEvent { + let mods = mods.into(); + let key = key.into_key(); + KeyEvent { + key, + code: Code::Unidentified, + location: Location::Standard, + state: KeyState::Down, + mods, + is_composing: false, + repeat: false, + } } } + +impl Modifiers { + pub const ALT: Modifiers = Modifiers(keyboard_types::Modifiers::ALT); + pub const ALT_GRAPH: Modifiers = Modifiers(keyboard_types::Modifiers::ALT_GRAPH); + pub const CAPS_LOCK: Modifiers = Modifiers(keyboard_types::Modifiers::CAPS_LOCK); + pub const CONTROL: Modifiers = Modifiers(keyboard_types::Modifiers::CONTROL); + pub const FN: Modifiers = Modifiers(keyboard_types::Modifiers::FN); + pub const FN_LOCK: Modifiers = Modifiers(keyboard_types::Modifiers::FN_LOCK); + pub const META: Modifiers = Modifiers(keyboard_types::Modifiers::META); + pub const NUM_LOCK: Modifiers = Modifiers(keyboard_types::Modifiers::NUM_LOCK); + pub const SCROLL_LOCK: Modifiers = Modifiers(keyboard_types::Modifiers::SCROLL_LOCK); + pub const SHIFT: Modifiers = Modifiers(keyboard_types::Modifiers::SHIFT); + pub const SYMBOL: Modifiers = Modifiers(keyboard_types::Modifiers::SYMBOL); + pub const SYMBOL_LOCK: Modifiers = Modifiers(keyboard_types::Modifiers::SYMBOL_LOCK); + pub const HYPER: Modifiers = Modifiers(keyboard_types::Modifiers::HYPER); + pub const SUPER: Modifiers = Modifiers(keyboard_types::Modifiers::SUPER); + + /// Get the inner value. + /// + /// Note that this function might go away if our changes are upstreamed. + pub fn raw(&self) -> keyboard_types::Modifiers { + self.0 + } + + /// Determine whether Shift is set. + pub fn shift(&self) -> bool { + self.contains(Modifiers::SHIFT) + } + + /// Determine whether Ctrl is set. + pub fn ctrl(&self) -> bool { + self.contains(Modifiers::CONTROL) + } + + /// Determine whether Alt is set. + pub fn alt(&self) -> bool { + self.contains(Modifiers::ALT) + } + + /// Determine whether Meta is set. + pub fn meta(&self) -> bool { + self.contains(Modifiers::META) + } + + /// Returns an empty set of modifiers. + pub fn empty() -> Modifiers { + Default::default() + } + + /// Returns `true` if no modifiers are set. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns `true` if all the modifiers in `other` are set. + pub fn contains(&self, other: Modifiers) -> bool { + self.0.contains(other.0) + } + + /// Inserts or removes the specified modifiers depending on the passed value. + pub fn set(&mut self, other: Modifiers, value: bool) { + self.0.set(other.0, value) + } +} + +impl BitAnd for Modifiers { + type Output = Self; + + fn bitand(self, rhs: Self) -> Self { + Modifiers(self.0 & rhs.0) + } +} + +impl BitAndAssign for Modifiers { + // rhs is the "right-hand side" of the expression `a &= b` + fn bitand_assign(&mut self, rhs: Self) { + *self = Modifiers(self.0 & rhs.0) + } +} + +impl BitOr for Modifiers { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self { + Modifiers(self.0 | rhs.0) + } +} + +impl BitOrAssign for Modifiers { + // rhs is the "right-hand side" of the expression `a &= b` + fn bitor_assign(&mut self, rhs: Self) { + *self = Modifiers(self.0 | rhs.0) + } +} + +impl BitXor for Modifiers { + type Output = Self; + + fn bitxor(self, rhs: Self) -> Self { + Modifiers(self.0 ^ rhs.0) + } +} + +impl BitXorAssign for Modifiers { + // rhs is the "right-hand side" of the expression `a &= b` + fn bitxor_assign(&mut self, rhs: Self) { + *self = Modifiers(self.0 ^ rhs.0) + } +} + +impl Not for Modifiers { + type Output = Self; + + fn not(self) -> Self { + Modifiers(!self.0) + } +} + +impl IntoKey for KbKey { + fn into_key(self) -> KbKey { + self + } +} + +impl IntoKey for &str { + fn into_key(self) -> KbKey { + KbKey::Character(self.into()) + } +} + + +#[cfg(any(all(feature = "x11", target_os = "linux"), target_os = "macos"))] +/// Map key code to location. +/// +/// The logic for this is adapted from InitKeyEvent in TextInputHandler (in the Mozilla +/// mac port). +/// +/// Note: in the original, this is based on kVK constants, but since we don't have those +/// readily available, we use the mapping to code (which should be effectively lossless). +pub fn code_to_location(code: Code) -> Location { + match code { + Code::MetaLeft | Code::ShiftLeft | Code::AltLeft | Code::ControlLeft => Location::Left, + Code::MetaRight | Code::ShiftRight | Code::AltRight | Code::ControlRight => Location::Right, + Code::Numpad0 + | Code::Numpad1 + | Code::Numpad2 + | Code::Numpad3 + | Code::Numpad4 + | Code::Numpad5 + | Code::Numpad6 + | Code::Numpad7 + | Code::Numpad8 + | Code::Numpad9 + | Code::NumpadAdd + | Code::NumpadComma + | Code::NumpadDecimal + | Code::NumpadDivide + | Code::NumpadEnter + | Code::NumpadEqual + | Code::NumpadMultiply + | Code::NumpadSubtract => Location::Numpad, + _ => Location::Standard, + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 642ac54..ef2f728 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ mod mouse_cursor; mod window_info; mod window_open_options; pub use event::*; -pub use keyboard::*; +pub use keyboard::KeyEvent; pub use mouse_cursor::MouseCursor; pub use window_info::*; pub use window_open_options::*; diff --git a/src/macos/keyboard.rs b/src/macos/keyboard.rs new file mode 100644 index 0000000..40cc511 --- /dev/null +++ b/src/macos/keyboard.rs @@ -0,0 +1,367 @@ +// Copyright 2020 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Baseview modifications to druid code: +// - move from_nsstring function to this file +// - update imports, paths etc + +//! Conversion of platform keyboard event into cross-platform event. + +use cocoa::appkit::{NSEvent, NSEventModifierFlags, NSEventType}; +use cocoa::foundation::NSString; +use cocoa::base::id; +use keyboard_types::{Code, KeyState}; +use objc::{msg_send, sel, sel_impl}; + +use crate::keyboard::{code_to_location, KbKey, KeyEvent, Modifiers}; + + +pub(crate) fn from_nsstring(s: id) -> String { + unsafe { + let slice = std::slice::from_raw_parts(s.UTF8String() as *const _, s.len()); + let result = std::str::from_utf8_unchecked(slice); + result.into() + } +} + +/// State for processing of keyboard events. +/// +/// This needs to be stateful for proper processing of dead keys. The current +/// implementation is somewhat primitive and is not based on IME; in the future +/// when IME is implemented, it will need to be redone somewhat, letting the IME +/// be the authoritative source of truth for Unicode string values of keys. +/// +/// Most of the logic in this module is adapted from Mozilla, and in particular +/// TextInputHandler.mm. +pub(crate) struct KeyboardState { + last_mods: NSEventModifierFlags, +} + +/// Convert a macOS platform key code (keyCode field of NSEvent). +/// +/// The primary source for this mapping is: +/// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values +/// +/// It should also match up with CODE_MAP_MAC bindings in +/// NativeKeyToDOMCodeName.h. +fn key_code_to_code(key_code: u16) -> Code { + match key_code { + 0x00 => Code::KeyA, + 0x01 => Code::KeyS, + 0x02 => Code::KeyD, + 0x03 => Code::KeyF, + 0x04 => Code::KeyH, + 0x05 => Code::KeyG, + 0x06 => Code::KeyZ, + 0x07 => Code::KeyX, + 0x08 => Code::KeyC, + 0x09 => Code::KeyV, + 0x0a => Code::IntlBackslash, + 0x0b => Code::KeyB, + 0x0c => Code::KeyQ, + 0x0d => Code::KeyW, + 0x0e => Code::KeyE, + 0x0f => Code::KeyR, + 0x10 => Code::KeyY, + 0x11 => Code::KeyT, + 0x12 => Code::Digit1, + 0x13 => Code::Digit2, + 0x14 => Code::Digit3, + 0x15 => Code::Digit4, + 0x16 => Code::Digit6, + 0x17 => Code::Digit5, + 0x18 => Code::Equal, + 0x19 => Code::Digit9, + 0x1a => Code::Digit7, + 0x1b => Code::Minus, + 0x1c => Code::Digit8, + 0x1d => Code::Digit0, + 0x1e => Code::BracketRight, + 0x1f => Code::KeyO, + 0x20 => Code::KeyU, + 0x21 => Code::BracketLeft, + 0x22 => Code::KeyI, + 0x23 => Code::KeyP, + 0x24 => Code::Enter, + 0x25 => Code::KeyL, + 0x26 => Code::KeyJ, + 0x27 => Code::Quote, + 0x28 => Code::KeyK, + 0x29 => Code::Semicolon, + 0x2a => Code::Backslash, + 0x2b => Code::Comma, + 0x2c => Code::Slash, + 0x2d => Code::KeyN, + 0x2e => Code::KeyM, + 0x2f => Code::Period, + 0x30 => Code::Tab, + 0x31 => Code::Space, + 0x32 => Code::Backquote, + 0x33 => Code::Backspace, + 0x34 => Code::NumpadEnter, + 0x35 => Code::Escape, + 0x36 => Code::MetaRight, + 0x37 => Code::MetaLeft, + 0x38 => Code::ShiftLeft, + 0x39 => Code::CapsLock, + // Note: in the linked source doc, this is "OSLeft" + 0x3a => Code::AltLeft, + 0x3b => Code::ControlLeft, + 0x3c => Code::ShiftRight, + // Note: in the linked source doc, this is "OSRight" + 0x3d => Code::AltRight, + 0x3e => Code::ControlRight, + 0x3f => Code::Fn, // No events fired + //0x40 => Code::F17, + 0x41 => Code::NumpadDecimal, + 0x43 => Code::NumpadMultiply, + 0x45 => Code::NumpadAdd, + 0x47 => Code::NumLock, + 0x48 => Code::AudioVolumeUp, + 0x49 => Code::AudioVolumeDown, + 0x4a => Code::AudioVolumeMute, + 0x4b => Code::NumpadDivide, + 0x4c => Code::NumpadEnter, + 0x4e => Code::NumpadSubtract, + //0x4f => Code::F18, + //0x50 => Code::F19, + 0x51 => Code::NumpadEqual, + 0x52 => Code::Numpad0, + 0x53 => Code::Numpad1, + 0x54 => Code::Numpad2, + 0x55 => Code::Numpad3, + 0x56 => Code::Numpad4, + 0x57 => Code::Numpad5, + 0x58 => Code::Numpad6, + 0x59 => Code::Numpad7, + //0x5a => Code::F20, + 0x5b => Code::Numpad8, + 0x5c => Code::Numpad9, + 0x5d => Code::IntlYen, + 0x5e => Code::IntlRo, + 0x5f => Code::NumpadComma, + 0x60 => Code::F5, + 0x61 => Code::F6, + 0x62 => Code::F7, + 0x63 => Code::F3, + 0x64 => Code::F8, + 0x65 => Code::F9, + 0x66 => Code::Lang2, + 0x67 => Code::F11, + 0x68 => Code::Lang1, + // Note: this is listed as F13, but in testing with a standard + // USB kb, this the code produced by PrtSc. + 0x69 => Code::PrintScreen, + //0x6a => Code::F16, + //0x6b => Code::F14, + 0x6d => Code::F10, + 0x6e => Code::ContextMenu, + 0x6f => Code::F12, + //0x71 => Code::F15, + 0x72 => Code::Help, + 0x73 => Code::Home, + 0x74 => Code::PageUp, + 0x75 => Code::Delete, + 0x76 => Code::F4, + 0x77 => Code::End, + 0x78 => Code::F2, + 0x79 => Code::PageDown, + 0x7a => Code::F1, + 0x7b => Code::ArrowLeft, + 0x7c => Code::ArrowRight, + 0x7d => Code::ArrowDown, + 0x7e => Code::ArrowUp, + _ => Code::Unidentified, + } +} + +/// Convert code to key. +/// +/// On macOS, for non-printable keys, the keyCode we get from the event serves is +/// really more of a key than a physical scan code. +/// +/// When this function returns None, the code can be considered printable. +/// +/// The logic for this function is derived from KEY_MAP_COCOA bindings in +/// NativeKeyToDOMKeyName.h. +fn code_to_key(code: Code) -> Option { + Some(match code { + Code::Escape => KbKey::Escape, + Code::ShiftLeft | Code::ShiftRight => KbKey::Shift, + Code::AltLeft | Code::AltRight => KbKey::Alt, + Code::MetaLeft | Code::MetaRight => KbKey::Meta, + Code::ControlLeft | Code::ControlRight => KbKey::Control, + Code::CapsLock => KbKey::CapsLock, + // kVK_ANSI_KeypadClear + Code::NumLock => KbKey::Clear, + Code::Fn => KbKey::Fn, + Code::F1 => KbKey::F1, + Code::F2 => KbKey::F2, + Code::F3 => KbKey::F3, + Code::F4 => KbKey::F4, + Code::F5 => KbKey::F5, + Code::F6 => KbKey::F6, + Code::F7 => KbKey::F7, + Code::F8 => KbKey::F8, + Code::F9 => KbKey::F9, + Code::F10 => KbKey::F10, + Code::F11 => KbKey::F11, + Code::F12 => KbKey::F12, + Code::Pause => KbKey::Pause, + Code::ScrollLock => KbKey::ScrollLock, + Code::PrintScreen => KbKey::PrintScreen, + Code::Insert => KbKey::Insert, + Code::Delete => KbKey::Delete, + Code::Tab => KbKey::Tab, + Code::Backspace => KbKey::Backspace, + Code::ContextMenu => KbKey::ContextMenu, + // kVK_JIS_Kana + Code::Lang1 => KbKey::KanjiMode, + // kVK_JIS_Eisu + Code::Lang2 => KbKey::Eisu, + Code::Home => KbKey::Home, + Code::End => KbKey::End, + Code::PageUp => KbKey::PageUp, + Code::PageDown => KbKey::PageDown, + Code::ArrowLeft => KbKey::ArrowLeft, + Code::ArrowRight => KbKey::ArrowRight, + Code::ArrowUp => KbKey::ArrowUp, + Code::ArrowDown => KbKey::ArrowDown, + Code::Enter => KbKey::Enter, + Code::NumpadEnter => KbKey::Enter, + Code::Help => KbKey::Help, + _ => return None, + }) +} + + +fn is_valid_key(s: &str) -> bool { + match s.chars().next() { + None => false, + Some(c) => c >= ' ' && c != '\x7f' && !('\u{e000}'..'\u{f900}').contains(&c), + } +} + + +fn is_modifier_code(code: Code) -> bool { + matches!( + code, + Code::ShiftLeft + | Code::ShiftRight + | Code::AltLeft + | Code::AltRight + | Code::ControlLeft + | Code::ControlRight + | Code::MetaLeft + | Code::MetaRight + | Code::CapsLock + | Code::Help + ) +} + + +impl KeyboardState { + pub(crate) fn new() -> KeyboardState { + let last_mods = NSEventModifierFlags::empty(); + KeyboardState { last_mods } + } + + pub(crate) fn process_native_event(&mut self, event: id) -> Option { + unsafe { + let event_type = event.eventType(); + let key_code = event.keyCode(); + let code = key_code_to_code(key_code); + let location = code_to_location(code); + let raw_mods = event.modifierFlags(); + let mods = make_modifiers(raw_mods); + let state = match event_type { + NSEventType::NSKeyDown => KeyState::Down, + NSEventType::NSKeyUp => KeyState::Up, + NSEventType::NSFlagsChanged => { + // We use `bits` here because we want to distinguish the + // device dependent bits (when both left and right keys + // may be pressed, for example). + let any_down = raw_mods.bits() & !self.last_mods.bits(); + self.last_mods = raw_mods; + if is_modifier_code(code) { + if any_down == 0 { + KeyState::Up + } else { + KeyState::Down + } + } else { + // HandleFlagsChanged has some logic for this; it might + // happen when an app is deactivated by Command-Tab. In + // that case, the best thing to do is synthesize the event + // from the modifiers. But a challenge there is that we + // might get multiple events. + return None; + } + } + _ => unreachable!(), + }; + let is_composing = false; + let repeat: bool = event_type == NSEventType::NSKeyDown && msg_send![event, isARepeat]; + let key = if let Some(key) = code_to_key(code) { + key + } else { + let characters = from_nsstring(event.characters()); + if is_valid_key(&characters) { + KbKey::Character(characters) + } else { + let chars_ignoring = from_nsstring(event.charactersIgnoringModifiers()); + if is_valid_key(&chars_ignoring) { + KbKey::Character(chars_ignoring) + } else { + // There may be more heroic things we can do here. + KbKey::Unidentified + } + } + }; + let event = KeyEvent { + code, + key, + location, + mods, + state, + is_composing, + repeat, + }; + Some(event) + } + } +} + + +const MODIFIER_MAP: &[(NSEventModifierFlags, Modifiers)] = &[ + (NSEventModifierFlags::NSShiftKeyMask, Modifiers::SHIFT), + (NSEventModifierFlags::NSAlternateKeyMask, Modifiers::ALT), + (NSEventModifierFlags::NSControlKeyMask, Modifiers::CONTROL), + (NSEventModifierFlags::NSCommandKeyMask, Modifiers::META), + ( + NSEventModifierFlags::NSAlphaShiftKeyMask, + Modifiers::CAPS_LOCK, + ), +]; + + +pub(crate) fn make_modifiers(raw: NSEventModifierFlags) -> Modifiers { + let mut modifiers = Modifiers::empty(); + for &(flags, mods) in MODIFIER_MAP { + if raw.contains(flags) { + modifiers |= mods; + } + } + modifiers +} \ No newline at end of file diff --git a/src/macos/mod.rs b/src/macos/mod.rs index c0efe14..181bc37 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -1,3 +1,4 @@ +mod keyboard; mod window; mod view; diff --git a/src/macos/view.rs b/src/macos/view.rs index d14373f..0de1dc2 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -15,7 +15,7 @@ use objc::{ use uuid::Uuid; use crate::{ - Event, MouseButton, MouseEvent, Point, WindowHandler, + Event, KeyboardEvent, MouseButton, MouseEvent, Point, WindowHandler, WindowOpenOptions }; use crate::MouseEvent::{ButtonPressed, ButtonReleased}; @@ -134,6 +134,15 @@ unsafe fn create_view_class() -> &'static Class { middle_mouse_up:: as extern "C" fn(&Object, Sel, id), ); + class.add_method( + sel!(keyDown:), + key_down:: as extern "C" fn(&Object, Sel, id), + ); + class.add_method( + sel!(keyUp:), + key_up:: as extern "C" fn(&Object, Sel, id), + ); + class.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR_NAME); class.register() @@ -333,4 +342,30 @@ mouse_simple_extern_fn!(middle_mouse_down, ButtonPressed(MouseButton::Middle)); mouse_simple_extern_fn!(middle_mouse_up, ButtonReleased(MouseButton::Middle)); mouse_simple_extern_fn!(mouse_entered, MouseEvent::CursorEntered); -mouse_simple_extern_fn!(mouse_exited, MouseEvent::CursorLeft); \ No newline at end of file +mouse_simple_extern_fn!(mouse_exited, MouseEvent::CursorLeft); + + +extern "C" fn key_down(this: &Object, _: Sel, event: id){ + let state: &mut WindowState = unsafe { + WindowState::from_field(this) + }; + + if let Some(key_event) = state.process_native_key_event(event){ + let event = Event::Keyboard(KeyboardEvent::KeyPressed(key_event)); + + state.trigger_event(event); + } +} + + +extern "C" fn key_up(this: &Object, _: Sel, event: id){ + let state: &mut WindowState = unsafe { + WindowState::from_field(this) + }; + + if let Some(key_event) = state.process_native_key_event(event){ + let event = Event::Keyboard(KeyboardEvent::KeyReleased(key_event)); + + state.trigger_event(event); + } +} \ No newline at end of file diff --git a/src/macos/window.rs b/src/macos/window.rs index 8901260..39dae1f 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -17,11 +17,12 @@ use objc::{msg_send, runtime::Object, sel, sel_impl}; use raw_window_handle::{macos::MacOSHandle, HasRawWindowHandle, RawWindowHandle}; use crate::{ - Event, Parent, WindowHandler, WindowOpenOptions, WindowScalePolicy, - WindowInfo + Event, keyboard::KeyEvent, Parent, WindowHandler, WindowOpenOptions, + WindowScalePolicy, WindowInfo }; use super::view::create_view; +use super::keyboard::KeyboardState; /// Name of the field used to store the `WindowState` pointer in the custom @@ -156,6 +157,7 @@ impl Window { let window_state_arc = Arc::new(WindowState { window, window_handler, + keyboard_state: KeyboardState::new(), }); let window_state_pointer = Arc::into_raw( @@ -177,6 +179,7 @@ impl Window { pub(super) struct WindowState { window: Window, window_handler: H, + keyboard_state: KeyboardState, } @@ -195,6 +198,13 @@ impl WindowState { pub(super) fn trigger_event(&mut self, event: Event){ self.window_handler.on_event(&mut self.window, event); } + + pub(super) fn process_native_key_event( + &mut self, + event: *mut Object + ) -> Option { + self.keyboard_state.process_native_event(event) + } } diff --git a/src/x11/keyboard.rs b/src/x11/keyboard.rs new file mode 100644 index 0000000..8b705cf --- /dev/null +++ b/src/x11/keyboard.rs @@ -0,0 +1,431 @@ +// Copyright 2020 The Druid Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Baseview modifications to druid code: +// - collect functions from various files +// - update imports, paths etc + +//! X11 keyboard handling + +use super::super::shared; +use crate::keyboard::code_to_location; +use crate::keyboard::{Code, KbKey, Modifiers}; +use x11rb::protocol::xproto::{Keycode, KeyPressEvent}; + +use crate::keyboard::KeyEvent; + + +/// Convert a hardware scan code to a key. +/// +/// Note: this is a hardcoded layout. We need to detect the user's +/// layout from the system and apply it. +fn code_to_key(code: Code, m: Modifiers) -> KbKey { + fn a(s: &str) -> KbKey { + KbKey::Character(s.into()) + } + fn s(mods: Modifiers, base: &str, shifted: &str) -> KbKey { + if mods.shift() { + KbKey::Character(shifted.into()) + } else { + KbKey::Character(base.into()) + } + } + fn n(mods: Modifiers, base: KbKey, num: &str) -> KbKey { + if mods.contains(Modifiers::NUM_LOCK) != mods.shift() { + KbKey::Character(num.into()) + } else { + base + } + } + match code { + Code::KeyA => s(m, "a", "A"), + Code::KeyB => s(m, "b", "B"), + Code::KeyC => s(m, "c", "C"), + Code::KeyD => s(m, "d", "D"), + Code::KeyE => s(m, "e", "E"), + Code::KeyF => s(m, "f", "F"), + Code::KeyG => s(m, "g", "G"), + Code::KeyH => s(m, "h", "H"), + Code::KeyI => s(m, "i", "I"), + Code::KeyJ => s(m, "j", "J"), + Code::KeyK => s(m, "k", "K"), + Code::KeyL => s(m, "l", "L"), + Code::KeyM => s(m, "m", "M"), + Code::KeyN => s(m, "n", "N"), + Code::KeyO => s(m, "o", "O"), + Code::KeyP => s(m, "p", "P"), + Code::KeyQ => s(m, "q", "Q"), + Code::KeyR => s(m, "r", "R"), + Code::KeyS => s(m, "s", "S"), + Code::KeyT => s(m, "t", "T"), + Code::KeyU => s(m, "u", "U"), + Code::KeyV => s(m, "v", "V"), + Code::KeyW => s(m, "w", "W"), + Code::KeyX => s(m, "x", "X"), + Code::KeyY => s(m, "y", "Y"), + Code::KeyZ => s(m, "z", "Z"), + + Code::Digit0 => s(m, "0", ")"), + Code::Digit1 => s(m, "1", "!"), + Code::Digit2 => s(m, "2", "@"), + Code::Digit3 => s(m, "3", "#"), + Code::Digit4 => s(m, "4", "$"), + Code::Digit5 => s(m, "5", "%"), + Code::Digit6 => s(m, "6", "^"), + Code::Digit7 => s(m, "7", "&"), + Code::Digit8 => s(m, "8", "*"), + Code::Digit9 => s(m, "9", "("), + + Code::Backquote => s(m, "`", "~"), + Code::Minus => s(m, "-", "_"), + Code::Equal => s(m, "=", "+"), + Code::BracketLeft => s(m, "[", "{"), + Code::BracketRight => s(m, "]", "}"), + Code::Backslash => s(m, "\\", "|"), + Code::Semicolon => s(m, ";", ":"), + Code::Quote => s(m, "'", "\""), + Code::Comma => s(m, ",", "<"), + Code::Period => s(m, ".", ">"), + Code::Slash => s(m, "/", "?"), + + Code::Space => a(" "), + + Code::Escape => KbKey::Escape, + Code::Backspace => KbKey::Backspace, + Code::Tab => KbKey::Tab, + Code::Enter => KbKey::Enter, + Code::ControlLeft => KbKey::Control, + Code::ShiftLeft => KbKey::Shift, + Code::ShiftRight => KbKey::Shift, + Code::NumpadMultiply => a("*"), + Code::AltLeft => KbKey::Alt, + Code::CapsLock => KbKey::CapsLock, + Code::F1 => KbKey::F1, + Code::F2 => KbKey::F2, + Code::F3 => KbKey::F3, + Code::F4 => KbKey::F4, + Code::F5 => KbKey::F5, + Code::F6 => KbKey::F6, + Code::F7 => KbKey::F7, + Code::F8 => KbKey::F8, + Code::F9 => KbKey::F9, + Code::F10 => KbKey::F10, + Code::NumLock => KbKey::NumLock, + Code::ScrollLock => KbKey::ScrollLock, + Code::Numpad0 => n(m, KbKey::Insert, "0"), + Code::Numpad1 => n(m, KbKey::End, "1"), + Code::Numpad2 => n(m, KbKey::ArrowDown, "2"), + Code::Numpad3 => n(m, KbKey::PageDown, "3"), + Code::Numpad4 => n(m, KbKey::ArrowLeft, "4"), + Code::Numpad5 => n(m, KbKey::Clear, "5"), + Code::Numpad6 => n(m, KbKey::ArrowRight, "6"), + Code::Numpad7 => n(m, KbKey::Home, "7"), + Code::Numpad8 => n(m, KbKey::ArrowUp, "8"), + Code::Numpad9 => n(m, KbKey::PageUp, "9"), + Code::NumpadSubtract => a("-"), + Code::NumpadAdd => a("+"), + Code::NumpadDecimal => n(m, KbKey::Delete, "."), + Code::IntlBackslash => s(m, "\\", "|"), + Code::F11 => KbKey::F11, + Code::F12 => KbKey::F12, + // This mapping is based on the picture in the w3c spec. + Code::IntlRo => a("\\"), + Code::Convert => KbKey::Convert, + Code::KanaMode => KbKey::KanaMode, + Code::NonConvert => KbKey::NonConvert, + Code::NumpadEnter => KbKey::Enter, + Code::ControlRight => KbKey::Control, + Code::NumpadDivide => a("/"), + Code::PrintScreen => KbKey::PrintScreen, + Code::AltRight => KbKey::Alt, + Code::Home => KbKey::Home, + Code::ArrowUp => KbKey::ArrowUp, + Code::PageUp => KbKey::PageUp, + Code::ArrowLeft => KbKey::ArrowLeft, + Code::ArrowRight => KbKey::ArrowRight, + Code::End => KbKey::End, + Code::ArrowDown => KbKey::ArrowDown, + Code::PageDown => KbKey::PageDown, + Code::Insert => KbKey::Insert, + Code::Delete => KbKey::Delete, + Code::AudioVolumeMute => KbKey::AudioVolumeMute, + Code::AudioVolumeDown => KbKey::AudioVolumeDown, + Code::AudioVolumeUp => KbKey::AudioVolumeUp, + Code::NumpadEqual => a("="), + Code::Pause => KbKey::Pause, + Code::NumpadComma => a(","), + Code::Lang1 => KbKey::HangulMode, + Code::Lang2 => KbKey::HanjaMode, + Code::IntlYen => a("¥"), + Code::MetaLeft => KbKey::Meta, + Code::MetaRight => KbKey::Meta, + Code::ContextMenu => KbKey::ContextMenu, + Code::BrowserStop => KbKey::BrowserStop, + Code::Again => KbKey::Again, + Code::Props => KbKey::Props, + Code::Undo => KbKey::Undo, + Code::Select => KbKey::Select, + Code::Copy => KbKey::Copy, + Code::Open => KbKey::Open, + Code::Paste => KbKey::Paste, + Code::Find => KbKey::Find, + Code::Cut => KbKey::Cut, + Code::Help => KbKey::Help, + Code::LaunchApp2 => KbKey::LaunchApplication2, + Code::WakeUp => KbKey::WakeUp, + Code::LaunchApp1 => KbKey::LaunchApplication1, + Code::LaunchMail => KbKey::LaunchMail, + Code::BrowserFavorites => KbKey::BrowserFavorites, + Code::BrowserBack => KbKey::BrowserBack, + Code::BrowserForward => KbKey::BrowserForward, + Code::Eject => KbKey::Eject, + Code::MediaTrackNext => KbKey::MediaTrackNext, + Code::MediaPlayPause => KbKey::MediaPlayPause, + Code::MediaTrackPrevious => KbKey::MediaTrackPrevious, + Code::MediaStop => KbKey::MediaStop, + Code::MediaSelect => KbKey::LaunchMediaPlayer, + Code::BrowserHome => KbKey::BrowserHome, + Code::BrowserRefresh => KbKey::BrowserRefresh, + Code::BrowserSearch => KbKey::BrowserSearch, + + _ => KbKey::Unidentified, + } +} + + +#[cfg(target_os = "linux")] +/// Map hardware keycode to code. +/// +/// In theory, the hardware keycode is device dependent, but in +/// practice it's probably pretty reliable. +/// +/// The logic is based on NativeKeyToDOMCodeName.h in Mozilla. +fn hardware_keycode_to_code(hw_keycode: u16) -> Code { + match hw_keycode { + 0x0009 => Code::Escape, + 0x000A => Code::Digit1, + 0x000B => Code::Digit2, + 0x000C => Code::Digit3, + 0x000D => Code::Digit4, + 0x000E => Code::Digit5, + 0x000F => Code::Digit6, + 0x0010 => Code::Digit7, + 0x0011 => Code::Digit8, + 0x0012 => Code::Digit9, + 0x0013 => Code::Digit0, + 0x0014 => Code::Minus, + 0x0015 => Code::Equal, + 0x0016 => Code::Backspace, + 0x0017 => Code::Tab, + 0x0018 => Code::KeyQ, + 0x0019 => Code::KeyW, + 0x001A => Code::KeyE, + 0x001B => Code::KeyR, + 0x001C => Code::KeyT, + 0x001D => Code::KeyY, + 0x001E => Code::KeyU, + 0x001F => Code::KeyI, + 0x0020 => Code::KeyO, + 0x0021 => Code::KeyP, + 0x0022 => Code::BracketLeft, + 0x0023 => Code::BracketRight, + 0x0024 => Code::Enter, + 0x0025 => Code::ControlLeft, + 0x0026 => Code::KeyA, + 0x0027 => Code::KeyS, + 0x0028 => Code::KeyD, + 0x0029 => Code::KeyF, + 0x002A => Code::KeyG, + 0x002B => Code::KeyH, + 0x002C => Code::KeyJ, + 0x002D => Code::KeyK, + 0x002E => Code::KeyL, + 0x002F => Code::Semicolon, + 0x0030 => Code::Quote, + 0x0031 => Code::Backquote, + 0x0032 => Code::ShiftLeft, + 0x0033 => Code::Backslash, + 0x0034 => Code::KeyZ, + 0x0035 => Code::KeyX, + 0x0036 => Code::KeyC, + 0x0037 => Code::KeyV, + 0x0038 => Code::KeyB, + 0x0039 => Code::KeyN, + 0x003A => Code::KeyM, + 0x003B => Code::Comma, + 0x003C => Code::Period, + 0x003D => Code::Slash, + 0x003E => Code::ShiftRight, + 0x003F => Code::NumpadMultiply, + 0x0040 => Code::AltLeft, + 0x0041 => Code::Space, + 0x0042 => Code::CapsLock, + 0x0043 => Code::F1, + 0x0044 => Code::F2, + 0x0045 => Code::F3, + 0x0046 => Code::F4, + 0x0047 => Code::F5, + 0x0048 => Code::F6, + 0x0049 => Code::F7, + 0x004A => Code::F8, + 0x004B => Code::F9, + 0x004C => Code::F10, + 0x004D => Code::NumLock, + 0x004E => Code::ScrollLock, + 0x004F => Code::Numpad7, + 0x0050 => Code::Numpad8, + 0x0051 => Code::Numpad9, + 0x0052 => Code::NumpadSubtract, + 0x0053 => Code::Numpad4, + 0x0054 => Code::Numpad5, + 0x0055 => Code::Numpad6, + 0x0056 => Code::NumpadAdd, + 0x0057 => Code::Numpad1, + 0x0058 => Code::Numpad2, + 0x0059 => Code::Numpad3, + 0x005A => Code::Numpad0, + 0x005B => Code::NumpadDecimal, + 0x005E => Code::IntlBackslash, + 0x005F => Code::F11, + 0x0060 => Code::F12, + 0x0061 => Code::IntlRo, + 0x0064 => Code::Convert, + 0x0065 => Code::KanaMode, + 0x0066 => Code::NonConvert, + 0x0068 => Code::NumpadEnter, + 0x0069 => Code::ControlRight, + 0x006A => Code::NumpadDivide, + 0x006B => Code::PrintScreen, + 0x006C => Code::AltRight, + 0x006E => Code::Home, + 0x006F => Code::ArrowUp, + 0x0070 => Code::PageUp, + 0x0071 => Code::ArrowLeft, + 0x0072 => Code::ArrowRight, + 0x0073 => Code::End, + 0x0074 => Code::ArrowDown, + 0x0075 => Code::PageDown, + 0x0076 => Code::Insert, + 0x0077 => Code::Delete, + 0x0079 => Code::AudioVolumeMute, + 0x007A => Code::AudioVolumeDown, + 0x007B => Code::AudioVolumeUp, + 0x007D => Code::NumpadEqual, + 0x007F => Code::Pause, + 0x0081 => Code::NumpadComma, + 0x0082 => Code::Lang1, + 0x0083 => Code::Lang2, + 0x0084 => Code::IntlYen, + 0x0085 => Code::MetaLeft, + 0x0086 => Code::MetaRight, + 0x0087 => Code::ContextMenu, + 0x0088 => Code::BrowserStop, + 0x0089 => Code::Again, + 0x008A => Code::Props, + 0x008B => Code::Undo, + 0x008C => Code::Select, + 0x008D => Code::Copy, + 0x008E => Code::Open, + 0x008F => Code::Paste, + 0x0090 => Code::Find, + 0x0091 => Code::Cut, + 0x0092 => Code::Help, + 0x0094 => Code::LaunchApp2, + 0x0097 => Code::WakeUp, + 0x0098 => Code::LaunchApp1, + // key to right of volume controls on T430s produces 0x9C + // but no documentation of what it should map to :/ + 0x00A3 => Code::LaunchMail, + 0x00A4 => Code::BrowserFavorites, + 0x00A6 => Code::BrowserBack, + 0x00A7 => Code::BrowserForward, + 0x00A9 => Code::Eject, + 0x00AB => Code::MediaTrackNext, + 0x00AC => Code::MediaPlayPause, + 0x00AD => Code::MediaTrackPrevious, + 0x00AE => Code::MediaStop, + 0x00B3 => Code::MediaSelect, + 0x00B4 => Code::BrowserHome, + 0x00B5 => Code::BrowserRefresh, + 0x00E1 => Code::BrowserSearch, + _ => Code::Unidentified, + } +} + +// Extracts the keyboard modifiers from, e.g., the `state` field of +// `xcb::xproto::ButtonPressEvent` +fn key_mods(mods: u16) -> Modifiers { + let mut ret = Modifiers::default(); + let mut key_masks = [ + (xproto::ModMask::Shift, Modifiers::SHIFT), + (xproto::ModMask::Control, Modifiers::CONTROL), + // X11's mod keys are configurable, but this seems + // like a reasonable default for US keyboards, at least, + // where the "windows" key seems to be MOD_MASK_4. + (xproto::ModMask::M1, Modifiers::ALT), + (xproto::ModMask::M2, Modifiers::NUM_LOCK), + (xproto::ModMask::M4, Modifiers::META), + (xproto::ModMask::Lock, Modifiers::CAPS_LOCK), + ]; + for (mask, modifiers) in &mut key_masks { + if mods & (*mask as u16) != 0 { + ret |= *modifiers; + } + } + ret +} + + +pub(super) fn convert_key_press_event( + key_press: &xcb::KeyPressEvent, +) -> KeyEvent { + let hw_keycode = key_press.detail(); + let code = hardware_keycode_to_code(hw_keycode); + let mods = key_mods(key_press.state()); + let key = code_to_key(code, mods); + let location = code_to_location(code); + let state = KeyState::Down; + + KeyEvent { + code, + key, + mods, + location, + state, + repeat: false, + is_composing: false, + } +} + + +pub(super) fn convert_key_release_event( + key_release: &xcb::KeyReleaseEvent +) -> KeyEvent { + let hw_keycode = key_release.detail(); + let code = hardware_keycode_to_code(hw_keycode); + let mods = key_mods(key_release.state()); + let key = code_to_key(code, mods); + let location = code_to_location(code); + let state = KeyState::Up; + + KeyEvent { + code, + key, + mods, + location, + state, + repeat: false, + is_composing: false, + } +} \ No newline at end of file diff --git a/src/x11/mod.rs b/src/x11/mod.rs index ecf4080..e87c5ac 100644 --- a/src/x11/mod.rs +++ b/src/x11/mod.rs @@ -4,4 +4,5 @@ use xcb_connection::XcbConnection; mod window; pub use window::*; -mod cursor; \ No newline at end of file +mod cursor; +mod keyboard; \ No newline at end of file diff --git a/src/x11/window.rs b/src/x11/window.rs index 84b31c4..a393668 100644 --- a/src/x11/window.rs +++ b/src/x11/window.rs @@ -11,8 +11,9 @@ use raw_window_handle::{ use super::XcbConnection; use crate::{ - Event, KeyboardEvent, MouseButton, MouseCursor, MouseEvent, Parent, ScrollDelta, WindowEvent, - WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, PhyPoint, PhySize, + Event, KeyboardEvent, MouseButton, MouseCursor, MouseEvent, Parent, + ScrollDelta, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, + WindowScalePolicy, PhyPoint, PhySize, }; pub struct Window { @@ -392,21 +393,27 @@ impl Window { //// xcb::KEY_PRESS => { let event = unsafe { xcb::cast_event::(&event) }; - let detail = event.detail(); + + let event = Event::Keyboard(KeyboardEvent::KeyPressed( + convert_key_press_event(&event) + )); handler.on_event( self, - Event::Keyboard(KeyboardEvent::KeyPressed(detail as u32)), + event ); } xcb::KEY_RELEASE => { let event = unsafe { xcb::cast_event::(&event) }; - let detail = event.detail(); + + let event = Event::Keyboard(KeyboardEvent::KeyReleased( + convert_key_release_event(&event) + )); handler.on_event( self, - Event::Keyboard(KeyboardEvent::KeyReleased(detail as u32)), + event ); }