platform.rs

  1use super::{BoolExt as _, Dispatcher, FontSystem, Window};
  2use crate::{executor, keymap::Keystroke, platform, ClipboardItem, Event, Menu, MenuItem};
  3use cocoa::{
  4    appkit::{
  5        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
  6        NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
  7        NSPasteboardTypeString, NSWindow,
  8    },
  9    base::{id, nil, selector},
 10    foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
 11};
 12use ctor::ctor;
 13use objc::{
 14    class,
 15    declare::ClassDecl,
 16    msg_send,
 17    runtime::{Class, Object, Sel},
 18    sel, sel_impl,
 19};
 20use ptr::null_mut;
 21use std::{
 22    any::Any,
 23    cell::RefCell,
 24    convert::TryInto,
 25    ffi::{c_void, CStr},
 26    os::raw::c_char,
 27    path::PathBuf,
 28    ptr,
 29    rc::Rc,
 30    slice, str,
 31    sync::Arc,
 32};
 33
 34const MAC_PLATFORM_IVAR: &'static str = "platform";
 35static mut APP_CLASS: *const Class = ptr::null();
 36static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
 37
 38#[ctor]
 39unsafe fn build_classes() {
 40    APP_CLASS = {
 41        let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
 42        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
 43        decl.add_method(
 44            sel!(sendEvent:),
 45            send_event as extern "C" fn(&mut Object, Sel, id),
 46        );
 47        decl.register()
 48    };
 49
 50    APP_DELEGATE_CLASS = {
 51        let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
 52        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
 53        decl.add_method(
 54            sel!(applicationDidFinishLaunching:),
 55            did_finish_launching as extern "C" fn(&mut Object, Sel, id),
 56        );
 57        decl.add_method(
 58            sel!(applicationDidBecomeActive:),
 59            did_become_active as extern "C" fn(&mut Object, Sel, id),
 60        );
 61        decl.add_method(
 62            sel!(applicationDidResignActive:),
 63            did_resign_active as extern "C" fn(&mut Object, Sel, id),
 64        );
 65        decl.add_method(
 66            sel!(handleGPUIMenuItem:),
 67            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
 68        );
 69        decl.add_method(
 70            sel!(application:openFiles:),
 71            open_files as extern "C" fn(&mut Object, Sel, id, id),
 72        );
 73        decl.register()
 74    }
 75}
 76
 77pub struct MacPlatform {
 78    dispatcher: Arc<Dispatcher>,
 79    fonts: Arc<FontSystem>,
 80    callbacks: RefCell<Callbacks>,
 81    menu_item_actions: RefCell<Vec<(String, Option<Box<dyn Any>>)>>,
 82    pasteboard: id,
 83    text_hash_pasteboard_type: id,
 84    metadata_pasteboard_type: id,
 85}
 86
 87#[derive(Default)]
 88struct Callbacks {
 89    become_active: Option<Box<dyn FnMut()>>,
 90    resign_active: Option<Box<dyn FnMut()>>,
 91    event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
 92    menu_command: Option<Box<dyn FnMut(&str, Option<&dyn Any>)>>,
 93    open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
 94    finish_launching: Option<Box<dyn FnOnce() -> ()>>,
 95}
 96
 97impl MacPlatform {
 98    pub fn new() -> Self {
 99        Self {
100            dispatcher: Arc::new(Dispatcher),
101            fonts: Arc::new(FontSystem::new()),
102            callbacks: Default::default(),
103            menu_item_actions: Default::default(),
104            pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
105            text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
106            metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
107        }
108    }
109
110    unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
111        let menu_bar = NSMenu::new(nil).autorelease();
112        let mut menu_item_actions = self.menu_item_actions.borrow_mut();
113        menu_item_actions.clear();
114
115        for menu_config in menus {
116            let menu_bar_item = NSMenuItem::new(nil).autorelease();
117            let menu = NSMenu::new(nil).autorelease();
118            let menu_name = menu_config.name;
119
120            menu.setTitle_(ns_string(menu_name));
121
122            for item_config in menu_config.items {
123                let item;
124
125                match item_config {
126                    MenuItem::Separator => {
127                        item = NSMenuItem::separatorItem(nil);
128                    }
129                    MenuItem::Action {
130                        name,
131                        keystroke,
132                        action,
133                        arg,
134                    } => {
135                        if let Some(keystroke) = keystroke {
136                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
137                                panic!(
138                                    "Invalid keystroke for menu item {}:{} - {:?}",
139                                    menu_name, name, err
140                                )
141                            });
142
143                            let mut mask = NSEventModifierFlags::empty();
144                            for (modifier, flag) in &[
145                                (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
146                                (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
147                                (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
148                            ] {
149                                if *modifier {
150                                    mask |= *flag;
151                                }
152                            }
153
154                            item = NSMenuItem::alloc(nil)
155                                .initWithTitle_action_keyEquivalent_(
156                                    ns_string(name),
157                                    selector("handleGPUIMenuItem:"),
158                                    ns_string(&keystroke.key),
159                                )
160                                .autorelease();
161                            item.setKeyEquivalentModifierMask_(mask);
162                        } else {
163                            item = NSMenuItem::alloc(nil)
164                                .initWithTitle_action_keyEquivalent_(
165                                    ns_string(name),
166                                    selector("handleGPUIMenuItem:"),
167                                    ns_string(""),
168                                )
169                                .autorelease();
170                        }
171
172                        let tag = menu_item_actions.len() as NSInteger;
173                        let _: () = msg_send![item, setTag: tag];
174                        menu_item_actions.push((action.to_string(), arg));
175                    }
176                }
177
178                menu.addItem_(item);
179            }
180
181            menu_bar_item.setSubmenu_(menu);
182            menu_bar.addItem_(menu_bar_item);
183        }
184
185        menu_bar
186    }
187
188    unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> {
189        let data = self.pasteboard.dataForType(kind);
190        if data == nil {
191            None
192        } else {
193            Some(slice::from_raw_parts(
194                data.bytes() as *mut u8,
195                data.length() as usize,
196            ))
197        }
198    }
199}
200
201impl platform::Platform for MacPlatform {
202    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
203        self.callbacks.borrow_mut().become_active = Some(callback);
204    }
205
206    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
207        self.callbacks.borrow_mut().resign_active = Some(callback);
208    }
209
210    fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
211        self.callbacks.borrow_mut().event = Some(callback);
212    }
213
214    fn on_menu_command(&self, callback: Box<dyn FnMut(&str, Option<&dyn Any>)>) {
215        self.callbacks.borrow_mut().menu_command = Some(callback);
216    }
217
218    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
219        self.callbacks.borrow_mut().open_files = Some(callback);
220    }
221
222    fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
223        self.callbacks.borrow_mut().finish_launching = Some(on_finish_launching);
224
225        unsafe {
226            let app: id = msg_send![APP_CLASS, sharedApplication];
227            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
228            app.setDelegate_(app_delegate);
229
230            let self_ptr = self as *const Self as *const c_void;
231            (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
232            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
233
234            let pool = NSAutoreleasePool::new(nil);
235            app.run();
236            pool.drain();
237
238            (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
239            (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
240        }
241    }
242
243    fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
244        self.dispatcher.clone()
245    }
246
247    fn activate(&self, ignoring_other_apps: bool) {
248        unsafe {
249            let app = NSApplication::sharedApplication(nil);
250            app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
251        }
252    }
253
254    fn open_window(
255        &self,
256        id: usize,
257        options: platform::WindowOptions,
258        executor: Rc<executor::Foreground>,
259    ) -> Box<dyn platform::Window> {
260        Box::new(Window::open(id, options, executor, self.fonts()))
261    }
262
263    fn key_window_id(&self) -> Option<usize> {
264        Window::key_window_id()
265    }
266
267    fn prompt_for_paths(
268        &self,
269        options: platform::PathPromptOptions,
270    ) -> Option<Vec<std::path::PathBuf>> {
271        unsafe {
272            let panel = NSOpenPanel::openPanel(nil);
273            panel.setCanChooseDirectories_(options.directories.to_objc());
274            panel.setCanChooseFiles_(options.files.to_objc());
275            panel.setAllowsMultipleSelection_(options.multiple.to_objc());
276            panel.setResolvesAliases_(false.to_objc());
277            let response = panel.runModal();
278            if response == NSModalResponse::NSModalResponseOk {
279                let mut result = Vec::new();
280                let urls = panel.URLs();
281                for i in 0..urls.count() {
282                    let url = urls.objectAtIndex(i);
283                    let string = url.absoluteString();
284                    let string = std::ffi::CStr::from_ptr(string.UTF8String())
285                        .to_string_lossy()
286                        .to_string();
287                    if let Some(path) = string.strip_prefix("file://") {
288                        result.push(PathBuf::from(path));
289                    }
290                }
291                Some(result)
292            } else {
293                None
294            }
295        }
296    }
297
298    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
299        self.fonts.clone()
300    }
301
302    fn quit(&self) {
303        unsafe {
304            let app = NSApplication::sharedApplication(nil);
305            let _: () = msg_send![app, terminate: nil];
306        }
307    }
308
309    fn write_to_clipboard(&self, item: ClipboardItem) {
310        unsafe {
311            self.pasteboard.clearContents();
312
313            let text_bytes = NSData::dataWithBytes_length_(
314                nil,
315                item.text.as_ptr() as *const c_void,
316                item.text.len() as u64,
317            );
318            self.pasteboard
319                .setData_forType(text_bytes, NSPasteboardTypeString);
320
321            if let Some(metadata) = item.metadata.as_ref() {
322                let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes();
323                let hash_bytes = NSData::dataWithBytes_length_(
324                    nil,
325                    hash_bytes.as_ptr() as *const c_void,
326                    hash_bytes.len() as u64,
327                );
328                self.pasteboard
329                    .setData_forType(hash_bytes, self.text_hash_pasteboard_type);
330
331                let metadata_bytes = NSData::dataWithBytes_length_(
332                    nil,
333                    metadata.as_ptr() as *const c_void,
334                    metadata.len() as u64,
335                );
336                self.pasteboard
337                    .setData_forType(metadata_bytes, self.metadata_pasteboard_type);
338            }
339        }
340    }
341
342    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
343        unsafe {
344            if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) {
345                let text = String::from_utf8_lossy(&text_bytes).to_string();
346                let hash_bytes = self
347                    .read_from_pasteboard(self.text_hash_pasteboard_type)
348                    .and_then(|bytes| bytes.try_into().ok())
349                    .map(u64::from_be_bytes);
350                let metadata_bytes = self
351                    .read_from_pasteboard(self.metadata_pasteboard_type)
352                    .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
353
354                if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) {
355                    if hash == ClipboardItem::text_hash(&text) {
356                        Some(ClipboardItem {
357                            text,
358                            metadata: Some(metadata),
359                        })
360                    } else {
361                        Some(ClipboardItem {
362                            text,
363                            metadata: None,
364                        })
365                    }
366                } else {
367                    Some(ClipboardItem {
368                        text,
369                        metadata: None,
370                    })
371                }
372            } else {
373                None
374            }
375        }
376    }
377
378    fn set_menus(&self, menus: Vec<Menu>) {
379        unsafe {
380            let app: id = msg_send![APP_CLASS, sharedApplication];
381            app.setMainMenu_(self.create_menu_bar(menus));
382        }
383    }
384}
385
386unsafe fn get_platform(object: &mut Object) -> &MacPlatform {
387    let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
388    assert!(!platform_ptr.is_null());
389    &*(platform_ptr as *const MacPlatform)
390}
391
392extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
393    unsafe {
394        if let Some(event) = Event::from_native(native_event, None) {
395            let platform = get_platform(this);
396            if let Some(callback) = platform.callbacks.borrow_mut().event.as_mut() {
397                if callback(event) {
398                    return;
399                }
400            }
401        }
402
403        msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
404    }
405}
406
407extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
408    unsafe {
409        let app: id = msg_send![APP_CLASS, sharedApplication];
410        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
411
412        let platform = get_platform(this);
413        if let Some(callback) = platform.callbacks.borrow_mut().finish_launching.take() {
414            callback();
415        }
416    }
417}
418
419extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
420    let platform = unsafe { get_platform(this) };
421    if let Some(callback) = platform.callbacks.borrow_mut().become_active.as_mut() {
422        callback();
423    }
424}
425
426extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
427    let platform = unsafe { get_platform(this) };
428    if let Some(callback) = platform.callbacks.borrow_mut().resign_active.as_mut() {
429        callback();
430    }
431}
432
433extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
434    let paths = unsafe {
435        (0..paths.count())
436            .into_iter()
437            .filter_map(|i| {
438                let path = paths.objectAtIndex(i);
439                match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
440                    Ok(string) => Some(PathBuf::from(string)),
441                    Err(err) => {
442                        log::error!("error converting path to string: {}", err);
443                        None
444                    }
445                }
446            })
447            .collect::<Vec<_>>()
448    };
449    let platform = unsafe { get_platform(this) };
450    if let Some(callback) = platform.callbacks.borrow_mut().open_files.as_mut() {
451        callback(paths);
452    }
453}
454
455extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
456    unsafe {
457        let platform = get_platform(this);
458        if let Some(callback) = platform.callbacks.borrow_mut().menu_command.as_mut() {
459            let tag: NSInteger = msg_send![item, tag];
460            let index = tag as usize;
461            if let Some((action, arg)) = platform.menu_item_actions.borrow().get(index) {
462                callback(action, arg.as_ref().map(Box::as_ref));
463            }
464        }
465    }
466}
467
468unsafe fn ns_string(string: &str) -> id {
469    NSString::alloc(nil).init_str(string).autorelease()
470}
471
472#[cfg(test)]
473mod tests {
474    use crate::platform::Platform;
475
476    use super::*;
477
478    #[test]
479    fn test_clipboard() {
480        let platform = build_platform();
481        assert_eq!(platform.read_from_clipboard(), None);
482
483        let item = ClipboardItem::new("1".to_string());
484        platform.write_to_clipboard(item.clone());
485        assert_eq!(platform.read_from_clipboard(), Some(item));
486
487        let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]);
488        platform.write_to_clipboard(item.clone());
489        assert_eq!(platform.read_from_clipboard(), Some(item));
490
491        let text_from_other_app = "text from other app";
492        unsafe {
493            let bytes = NSData::dataWithBytes_length_(
494                nil,
495                text_from_other_app.as_ptr() as *const c_void,
496                text_from_other_app.len() as u64,
497            );
498            platform
499                .pasteboard
500                .setData_forType(bytes, NSPasteboardTypeString);
501        }
502        assert_eq!(
503            platform.read_from_clipboard(),
504            Some(ClipboardItem::new(text_from_other_app.to_string()))
505        );
506    }
507
508    fn build_platform() -> MacPlatform {
509        let mut platform = MacPlatform::new();
510        platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
511        platform
512    }
513}