StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use cocoa::foundation::NSUInteger; |
| 2 | use std::{ffi::CStr, os::raw::c_char}; |
| 3 | |
| 4 | use strato_ui_core::event::{KeyEventDetails, ModifiersState}; |
| 5 | use strato_ui_core::platform::keyboard::{KeyCode, PhysicalKey}; |
| 6 | use strato_ui_core::{keymap::Keystroke, Event}; |
| 7 | |
| 8 | use cocoa::{ |
| 9 | appkit::{NSEvent, NSEventModifierFlags, NSEventType}, |
| 10 | base::{id, YES}, |
| 11 | foundation::NSString, |
| 12 | }; |
| 13 | use pathfinder_geometry::vector::vec2f; |
| 14 | |
| 15 | use super::{ |
| 16 | keycode::{scancode_to_physicalkey, Keycode}, |
| 17 | utils::unicode_char_to_key, |
| 18 | }; |
| 19 | |
| 20 | // Unpublished but widely known and stable flags for distinguishing left/right alt. |
| 21 | // Google "NX_DEVICELALTKEYMASK" for more. |
| 22 | const LEFT_ALT_MASK: NSUInteger = 0x00000020; |
| 23 | const RIGHT_ALT_MASK: NSUInteger = 0x00000040; |
| 24 | |
| 25 | fn modifier_flags_to_state(flags: NSEventModifierFlags) -> ModifiersState { |
| 26 | ModifiersState { |
| 27 | alt: flags.contains(NSEventModifierFlags::NSAlternateKeyMask), |
| 28 | cmd: flags.contains(NSEventModifierFlags::NSCommandKeyMask), |
| 29 | shift: flags.contains(NSEventModifierFlags::NSShiftKeyMask), |
| 30 | ctrl: flags.contains(NSEventModifierFlags::NSControlKeyMask), |
| 31 | func: flags.contains(NSEventModifierFlags::NSFunctionKeyMask), |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | fn native_key_code_to_key_code(native_key_code: u16) -> Option<KeyCode> { |
| 36 | let physical_key = scancode_to_physicalkey(native_key_code as u32); |
| 37 | match physical_key { |
| 38 | PhysicalKey::Code(key_code) => Some(key_code), |
| 39 | _ => None, |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | /// # Safety |
| 44 | /// This code is only unsafe since it requires interfacing with platform code. |
| 45 | /// Creates an event from a native event, taking in the current window_height and whether this is |
| 46 | /// the first mouse event on an inactive window that is causing the window to activate. |
| 47 | pub unsafe fn from_native( |
| 48 | native_event: id, |
| 49 | window_height: Option<f32>, |
| 50 | is_first_mouse: bool, |
| 51 | ) -> Option<Event> { |
| 52 | let event_type = native_event.eventType(); |
| 53 | |
| 54 | // Filter out event types that aren't in the NSEventType enum. |
| 55 | // See https://github.com/servo/cocoa-rs/issues/155#issuecomment-323482792 for details. |
| 56 | match event_type as u64 { |
| 57 | 0 | 21 | 32 | 33 | 35 | 36 | 37 => { |
| 58 | return None; |
| 59 | } |
| 60 | _ => {} |
| 61 | } |
| 62 | let modifiers = modifier_flags_to_state(native_event.modifierFlags()); |
| 63 | |
| 64 | match event_type { |
| 65 | NSEventType::NSKeyDown => { |
| 66 | let native_modifiers = native_event.modifierFlags(); |
| 67 | |
| 68 | // Get the base character for this key without any modifiers (including Shift) |
| 69 | // using UCKeyTranslate via the platform's keyCodeToChar function. |
| 70 | // For example, Shift+1 on a US keyboard gives '!' as the key, but |
| 71 | // key_without_modifiers will be '1'. |
| 72 | let key_without_modifiers = Keycode(native_event.keyCode()).try_to_key_name(false); |
| 73 | |
| 74 | let details = KeyEventDetails { |
| 75 | left_alt: (native_modifiers.bits() & LEFT_ALT_MASK) != 0, |
| 76 | right_alt: (native_modifiers.bits() & RIGHT_ALT_MASK) != 0, |
| 77 | key_without_modifiers, |
| 78 | }; |
| 79 | let unmodified_chars = native_event.charactersIgnoringModifiers(); |
| 80 | let unmodified_chars = CStr::from_ptr(unmodified_chars.UTF8String() as *mut c_char) |
| 81 | .to_str() |
| 82 | .ok()?; |
| 83 | |
| 84 | let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() { |
| 85 | unicode_char_to_key(first_char as u16).unwrap_or(unmodified_chars) |
| 86 | } else { |
| 87 | return None; |
| 88 | }; |
| 89 | |
| 90 | let keystroke = Keystroke { |
| 91 | ctrl: native_modifiers.contains(NSEventModifierFlags::NSControlKeyMask), |
| 92 | alt: native_modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask), |
| 93 | shift: native_modifiers.contains(NSEventModifierFlags::NSShiftKeyMask), |
| 94 | cmd: native_modifiers.contains(NSEventModifierFlags::NSCommandKeyMask), |
| 95 | meta: false, /* handled separately */ |
| 96 | key: unmodified_chars.into(), |
| 97 | }; |
| 98 | |
| 99 | let chars = native_event.characters().UTF8String() as *mut c_char; |
| 100 | let chars = if chars.is_null() { |
| 101 | // `UTF8String` can return null in some rare cases where the |
| 102 | // string isn't valid UTF-8. For example, if the user |
| 103 | // enters a UTF-8 surrogate character, e.g. U+DDDD, via the |
| 104 | // Unicode Hex Input keyboard, the conversion will produce |
| 105 | // null. |
| 106 | String::new() |
| 107 | } else { |
| 108 | CStr::from_ptr(chars).to_str().ok()?.to_owned() |
| 109 | }; |
| 110 | |
| 111 | Some(Event::KeyDown { |
| 112 | keystroke, |
| 113 | chars, |
| 114 | details, |
| 115 | is_composing: false, |
| 116 | }) |
| 117 | } |
| 118 | NSEventType::NSMouseMoved => window_height.map(|window_height| Event::MouseMoved { |
| 119 | position: vec2f( |
| 120 | native_event.locationInWindow().x as f32, |
| 121 | window_height - native_event.locationInWindow().y as f32, |
| 122 | ), |
| 123 | cmd: native_event |
| 124 | .modifierFlags() |
| 125 | .contains(NSEventModifierFlags::NSCommandKeyMask), |
| 126 | shift: native_event |
| 127 | .modifierFlags() |
| 128 | .contains(NSEventModifierFlags::NSShiftKeyMask), |
| 129 | is_synthetic: false, |
| 130 | }), |
| 131 | NSEventType::NSFlagsChanged => { |
| 132 | let key_code = native_key_code_to_key_code(native_event.keyCode()); |
| 133 | |
| 134 | window_height.map(|window_height| Event::ModifierStateChanged { |
| 135 | mouse_position: vec2f( |
| 136 | native_event.locationInWindow().x as f32, |
| 137 | window_height - native_event.locationInWindow().y as f32, |
| 138 | ), |
| 139 | modifiers, |
| 140 | key_code, |
| 141 | }) |
| 142 | } |
| 143 | NSEventType::NSLeftMouseDown => window_height.map(|window_height| { |
| 144 | let position = vec2f( |
| 145 | native_event.locationInWindow().x as f32, |
| 146 | window_height - native_event.locationInWindow().y as f32, |
| 147 | ); |
| 148 | let click_count = native_event.clickCount() as u32; |
| 149 | |
| 150 | // ctrl-click should actually be registered as a right-click |
| 151 | // https://support.apple.com/guide/mac-help/right-click-mh35853/mac |
| 152 | if modifiers.ctrl { |
| 153 | Event::RightMouseDown { |
| 154 | position, |
| 155 | cmd: modifiers.cmd, |
| 156 | shift: modifiers.shift, |
| 157 | click_count, |
| 158 | } |
| 159 | } else { |
| 160 | Event::LeftMouseDown { |
| 161 | position, |
| 162 | modifiers, |
| 163 | click_count, |
| 164 | is_first_mouse, |
| 165 | } |
| 166 | } |
| 167 | }), |
| 168 | NSEventType::NSLeftMouseUp => window_height.map(|window_height| Event::LeftMouseUp { |
| 169 | position: vec2f( |
| 170 | native_event.locationInWindow().x as f32, |
| 171 | window_height - native_event.locationInWindow().y as f32, |
| 172 | ), |
| 173 | modifiers, |
| 174 | }), |
| 175 | NSEventType::NSLeftMouseDragged => { |
| 176 | window_height.map(|window_height| Event::LeftMouseDragged { |
| 177 | position: vec2f( |
| 178 | native_event.locationInWindow().x as f32, |
| 179 | window_height - native_event.locationInWindow().y as f32, |
| 180 | ), |
| 181 | modifiers, |
| 182 | }) |
| 183 | } |
| 184 | // TODO: This option is deprecated by Apple in favour of NSEventTypeOtherMouseDown |
| 185 | // but we'll likely need to update cocoa. |
| 186 | // See https://developer.apple.com/documentation/appkit/nsothermousedown. |
| 187 | NSEventType::NSOtherMouseDown => { |
| 188 | let window_height = window_height?; |
| 189 | let window_location = native_event.locationInWindow(); |
| 190 | let position = vec2f( |
| 191 | window_location.x as f32, |
| 192 | window_height - (window_location.y as f32), |
| 193 | ); |
| 194 | let modifier_flags = native_event.modifierFlags(); |
| 195 | let cmd = modifier_flags.contains(NSEventModifierFlags::NSCommandKeyMask); |
| 196 | let shift = modifier_flags.contains(NSEventModifierFlags::NSShiftKeyMask); |
| 197 | let click_count = native_event.clickCount() as u32; |
| 198 | |
| 199 | match native_event.buttonNumber() { |
| 200 | 2 => Some(Event::MiddleMouseDown { |
| 201 | position, |
| 202 | cmd, |
| 203 | shift, |
| 204 | click_count, |
| 205 | }), |
| 206 | 3 => Some(Event::BackMouseDown { |
| 207 | position, |
| 208 | cmd, |
| 209 | shift, |
| 210 | click_count, |
| 211 | }), |
| 212 | 4 => Some(Event::ForwardMouseDown { |
| 213 | position, |
| 214 | cmd, |
| 215 | shift, |
| 216 | click_count, |
| 217 | }), |
| 218 | _ => None, |
| 219 | } |
| 220 | } |
| 221 | // For trackpads, this event will get triggered by the user's secondary click setting. |
| 222 | NSEventType::NSRightMouseDown => window_height.map(|window_height| Event::RightMouseDown { |
| 223 | position: vec2f( |
| 224 | native_event.locationInWindow().x as f32, |
| 225 | window_height - native_event.locationInWindow().y as f32, |
| 226 | ), |
| 227 | cmd: native_event |
| 228 | .modifierFlags() |
| 229 | .contains(NSEventModifierFlags::NSCommandKeyMask), |
| 230 | shift: native_event |
| 231 | .modifierFlags() |
| 232 | .contains(NSEventModifierFlags::NSShiftKeyMask), |
| 233 | click_count: native_event.clickCount() as u32, |
| 234 | }), |
| 235 | NSEventType::NSScrollWheel => window_height.map(|window_height| Event::ScrollWheel { |
| 236 | position: vec2f( |
| 237 | native_event.locationInWindow().x as f32, |
| 238 | window_height - native_event.locationInWindow().y as f32, |
| 239 | ), |
| 240 | delta: vec2f( |
| 241 | native_event.scrollingDeltaX() as f32, |
| 242 | native_event.scrollingDeltaY() as f32, |
| 243 | ), |
| 244 | precise: native_event.hasPreciseScrollingDeltas() == YES, |
| 245 | modifiers, |
| 246 | }), |
| 247 | _ => None, |
| 248 | } |
| 249 | } |
| 250 |