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}