platform.rs

  1use super::{BoolExt as _, Dispatcher, FontSystem, Window};
  2use crate::{
  3    executor, keymap::Keystroke, platform, AnyAction, ClipboardItem, Event, Menu, MenuItem,
  4};
  5use block::ConcreteBlock;
  6use cocoa::{
  7    appkit::{
  8        NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
  9        NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
 10        NSPasteboardTypeString, NSSavePanel, NSWindow,
 11    },
 12    base::{id, nil, selector, YES},
 13    foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
 14};
 15use core_foundation::{
 16    base::{CFType, CFTypeRef, OSStatus, TCFType as _},
 17    boolean::CFBoolean,
 18    data::CFData,
 19    dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
 20    string::{CFString, CFStringRef},
 21};
 22use ctor::ctor;
 23use objc::{
 24    class,
 25    declare::ClassDecl,
 26    msg_send,
 27    runtime::{Class, Object, Sel},
 28    sel, sel_impl,
 29};
 30use ptr::null_mut;
 31use std::{
 32    any::Any,
 33    cell::{Cell, RefCell},
 34    convert::TryInto,
 35    ffi::{c_void, CStr},
 36    os::raw::c_char,
 37    path::{Path, PathBuf},
 38    ptr,
 39    rc::Rc,
 40    slice, str,
 41    sync::Arc,
 42};
 43
 44const MAC_PLATFORM_IVAR: &'static str = "platform";
 45static mut APP_CLASS: *const Class = ptr::null();
 46static mut APP_DELEGATE_CLASS: *const Class = ptr::null();
 47
 48#[ctor]
 49unsafe fn build_classes() {
 50    APP_CLASS = {
 51        let mut decl = ClassDecl::new("GPUIApplication", class!(NSApplication)).unwrap();
 52        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
 53        decl.add_method(
 54            sel!(sendEvent:),
 55            send_event as extern "C" fn(&mut Object, Sel, id),
 56        );
 57        decl.register()
 58    };
 59
 60    APP_DELEGATE_CLASS = {
 61        let mut decl = ClassDecl::new("GPUIApplicationDelegate", class!(NSResponder)).unwrap();
 62        decl.add_ivar::<*mut c_void>(MAC_PLATFORM_IVAR);
 63        decl.add_method(
 64            sel!(applicationDidFinishLaunching:),
 65            did_finish_launching as extern "C" fn(&mut Object, Sel, id),
 66        );
 67        decl.add_method(
 68            sel!(applicationDidBecomeActive:),
 69            did_become_active as extern "C" fn(&mut Object, Sel, id),
 70        );
 71        decl.add_method(
 72            sel!(applicationDidResignActive:),
 73            did_resign_active as extern "C" fn(&mut Object, Sel, id),
 74        );
 75        decl.add_method(
 76            sel!(handleGPUIMenuItem:),
 77            handle_menu_item as extern "C" fn(&mut Object, Sel, id),
 78        );
 79        decl.add_method(
 80            sel!(application:openFiles:),
 81            open_files as extern "C" fn(&mut Object, Sel, id, id),
 82        );
 83        decl.register()
 84    }
 85}
 86
 87#[derive(Default)]
 88pub struct MacForegroundPlatform(RefCell<MacForegroundPlatformState>);
 89
 90#[derive(Default)]
 91pub struct MacForegroundPlatformState {
 92    become_active: Option<Box<dyn FnMut()>>,
 93    resign_active: Option<Box<dyn FnMut()>>,
 94    event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
 95    menu_command: Option<Box<dyn FnMut(&dyn AnyAction)>>,
 96    open_files: Option<Box<dyn FnMut(Vec<PathBuf>)>>,
 97    finish_launching: Option<Box<dyn FnOnce() -> ()>>,
 98    menu_actions: Vec<Box<dyn AnyAction>>,
 99}
100
101impl MacForegroundPlatform {
102    unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
103        let menu_bar = NSMenu::new(nil).autorelease();
104        let mut state = self.0.borrow_mut();
105
106        state.menu_actions.clear();
107
108        for menu_config in menus {
109            let menu_bar_item = NSMenuItem::new(nil).autorelease();
110            let menu = NSMenu::new(nil).autorelease();
111            let menu_name = menu_config.name;
112
113            menu.setTitle_(ns_string(menu_name));
114
115            for item_config in menu_config.items {
116                let item;
117
118                match item_config {
119                    MenuItem::Separator => {
120                        item = NSMenuItem::separatorItem(nil);
121                    }
122                    MenuItem::Action {
123                        name,
124                        keystroke,
125                        action,
126                    } => {
127                        if let Some(keystroke) = keystroke {
128                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
129                                panic!(
130                                    "Invalid keystroke for menu item {}:{} - {:?}",
131                                    menu_name, name, err
132                                )
133                            });
134
135                            let mut mask = NSEventModifierFlags::empty();
136                            for (modifier, flag) in &[
137                                (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
138                                (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask),
139                                (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask),
140                            ] {
141                                if *modifier {
142                                    mask |= *flag;
143                                }
144                            }
145
146                            item = NSMenuItem::alloc(nil)
147                                .initWithTitle_action_keyEquivalent_(
148                                    ns_string(name),
149                                    selector("handleGPUIMenuItem:"),
150                                    ns_string(&keystroke.key),
151                                )
152                                .autorelease();
153                            item.setKeyEquivalentModifierMask_(mask);
154                        } else {
155                            item = NSMenuItem::alloc(nil)
156                                .initWithTitle_action_keyEquivalent_(
157                                    ns_string(name),
158                                    selector("handleGPUIMenuItem:"),
159                                    ns_string(""),
160                                )
161                                .autorelease();
162                        }
163
164                        let tag = state.menu_actions.len() as NSInteger;
165                        let _: () = msg_send![item, setTag: tag];
166                        state.menu_actions.push(action);
167                    }
168                }
169
170                menu.addItem_(item);
171            }
172
173            menu_bar_item.setSubmenu_(menu);
174            menu_bar.addItem_(menu_bar_item);
175        }
176
177        menu_bar
178    }
179}
180
181impl platform::ForegroundPlatform for MacForegroundPlatform {
182    fn on_become_active(&self, callback: Box<dyn FnMut()>) {
183        self.0.borrow_mut().become_active = Some(callback);
184    }
185
186    fn on_resign_active(&self, callback: Box<dyn FnMut()>) {
187        self.0.borrow_mut().resign_active = Some(callback);
188    }
189
190    fn on_event(&self, callback: Box<dyn FnMut(crate::Event) -> bool>) {
191        self.0.borrow_mut().event = Some(callback);
192    }
193
194    fn on_open_files(&self, callback: Box<dyn FnMut(Vec<PathBuf>)>) {
195        self.0.borrow_mut().open_files = Some(callback);
196    }
197
198    fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>) {
199        self.0.borrow_mut().finish_launching = Some(on_finish_launching);
200
201        unsafe {
202            let app: id = msg_send![APP_CLASS, sharedApplication];
203            let app_delegate: id = msg_send![APP_DELEGATE_CLASS, new];
204            app.setDelegate_(app_delegate);
205
206            let self_ptr = self as *const Self as *const c_void;
207            (*app).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
208            (*app_delegate).set_ivar(MAC_PLATFORM_IVAR, self_ptr);
209
210            let pool = NSAutoreleasePool::new(nil);
211            app.run();
212            pool.drain();
213
214            (*app).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
215            (*app.delegate()).set_ivar(MAC_PLATFORM_IVAR, null_mut::<c_void>());
216        }
217    }
218
219    fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn AnyAction)>) {
220        self.0.borrow_mut().menu_command = Some(callback);
221    }
222
223    fn set_menus(&self, menus: Vec<Menu>) {
224        unsafe {
225            let app: id = msg_send![APP_CLASS, sharedApplication];
226            app.setMainMenu_(self.create_menu_bar(menus));
227        }
228    }
229
230    fn prompt_for_paths(
231        &self,
232        options: platform::PathPromptOptions,
233        done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
234    ) {
235        unsafe {
236            let panel = NSOpenPanel::openPanel(nil);
237            panel.setCanChooseDirectories_(options.directories.to_objc());
238            panel.setCanChooseFiles_(options.files.to_objc());
239            panel.setAllowsMultipleSelection_(options.multiple.to_objc());
240            panel.setResolvesAliases_(false.to_objc());
241            let done_fn = Cell::new(Some(done_fn));
242            let block = ConcreteBlock::new(move |response: NSModalResponse| {
243                let result = if response == NSModalResponse::NSModalResponseOk {
244                    let mut result = Vec::new();
245                    let urls = panel.URLs();
246                    for i in 0..urls.count() {
247                        let url = urls.objectAtIndex(i);
248                        if url.isFileURL() == YES {
249                            let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
250                                .to_string_lossy()
251                                .to_string();
252                            result.push(PathBuf::from(path));
253                        }
254                    }
255                    Some(result)
256                } else {
257                    None
258                };
259
260                if let Some(done_fn) = done_fn.take() {
261                    (done_fn)(result);
262                }
263            });
264            let block = block.copy();
265            let _: () = msg_send![panel, beginWithCompletionHandler: block];
266        }
267    }
268
269    fn prompt_for_new_path(
270        &self,
271        directory: &Path,
272        done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
273    ) {
274        unsafe {
275            let panel = NSSavePanel::savePanel(nil);
276            let path = ns_string(directory.to_string_lossy().as_ref());
277            let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
278            panel.setDirectoryURL(url);
279
280            let done_fn = Cell::new(Some(done_fn));
281            let block = ConcreteBlock::new(move |response: NSModalResponse| {
282                let result = if response == NSModalResponse::NSModalResponseOk {
283                    let url = panel.URL();
284                    if url.isFileURL() == YES {
285                        let path = std::ffi::CStr::from_ptr(url.path().UTF8String())
286                            .to_string_lossy()
287                            .to_string();
288                        Some(PathBuf::from(path))
289                    } else {
290                        None
291                    }
292                } else {
293                    None
294                };
295
296                if let Some(done_fn) = done_fn.take() {
297                    (done_fn)(result);
298                }
299            });
300            let block = block.copy();
301            let _: () = msg_send![panel, beginWithCompletionHandler: block];
302        }
303    }
304}
305
306pub struct MacPlatform {
307    dispatcher: Arc<Dispatcher>,
308    fonts: Arc<FontSystem>,
309    pasteboard: id,
310    text_hash_pasteboard_type: id,
311    metadata_pasteboard_type: id,
312}
313
314impl MacPlatform {
315    pub fn new() -> Self {
316        Self {
317            dispatcher: Arc::new(Dispatcher),
318            fonts: Arc::new(FontSystem::new()),
319            pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
320            text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
321            metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
322        }
323    }
324
325    unsafe fn read_from_pasteboard(&self, kind: id) -> Option<&[u8]> {
326        let data = self.pasteboard.dataForType(kind);
327        if data == nil {
328            None
329        } else {
330            Some(slice::from_raw_parts(
331                data.bytes() as *mut u8,
332                data.length() as usize,
333            ))
334        }
335    }
336}
337
338unsafe impl Send for MacPlatform {}
339unsafe impl Sync for MacPlatform {}
340
341impl platform::Platform for MacPlatform {
342    fn dispatcher(&self) -> Arc<dyn platform::Dispatcher> {
343        self.dispatcher.clone()
344    }
345
346    fn activate(&self, ignoring_other_apps: bool) {
347        unsafe {
348            let app = NSApplication::sharedApplication(nil);
349            app.activateIgnoringOtherApps_(ignoring_other_apps.to_objc());
350        }
351    }
352
353    fn open_window(
354        &self,
355        id: usize,
356        options: platform::WindowOptions,
357        executor: Rc<executor::Foreground>,
358    ) -> Box<dyn platform::Window> {
359        Box::new(Window::open(id, options, executor, self.fonts()))
360    }
361
362    fn key_window_id(&self) -> Option<usize> {
363        Window::key_window_id()
364    }
365
366    fn fonts(&self) -> Arc<dyn platform::FontSystem> {
367        self.fonts.clone()
368    }
369
370    fn quit(&self) {
371        // Quitting the app causes us to close windows, which invokes `Window::on_close` callbacks
372        // synchronously before this method terminates. If we call `Platform::quit` while holding a
373        // borrow of the app state (which most of the time we will do), we will end up
374        // double-borrowing the app state in the `on_close` callbacks for our open windows. To solve
375        // this, we make quitting the application asynchronous so that we aren't holding borrows to
376        // the app state on the stack when we actually terminate the app.
377
378        use super::dispatcher::{dispatch_async_f, dispatch_get_main_queue};
379
380        unsafe {
381            dispatch_async_f(dispatch_get_main_queue(), ptr::null_mut(), Some(quit));
382        }
383
384        unsafe extern "C" fn quit(_: *mut c_void) {
385            let app = NSApplication::sharedApplication(nil);
386            let _: () = msg_send![app, terminate: nil];
387        }
388    }
389
390    fn write_to_clipboard(&self, item: ClipboardItem) {
391        unsafe {
392            self.pasteboard.clearContents();
393
394            let text_bytes = NSData::dataWithBytes_length_(
395                nil,
396                item.text.as_ptr() as *const c_void,
397                item.text.len() as u64,
398            );
399            self.pasteboard
400                .setData_forType(text_bytes, NSPasteboardTypeString);
401
402            if let Some(metadata) = item.metadata.as_ref() {
403                let hash_bytes = ClipboardItem::text_hash(&item.text).to_be_bytes();
404                let hash_bytes = NSData::dataWithBytes_length_(
405                    nil,
406                    hash_bytes.as_ptr() as *const c_void,
407                    hash_bytes.len() as u64,
408                );
409                self.pasteboard
410                    .setData_forType(hash_bytes, self.text_hash_pasteboard_type);
411
412                let metadata_bytes = NSData::dataWithBytes_length_(
413                    nil,
414                    metadata.as_ptr() as *const c_void,
415                    metadata.len() as u64,
416                );
417                self.pasteboard
418                    .setData_forType(metadata_bytes, self.metadata_pasteboard_type);
419            }
420        }
421    }
422
423    fn read_from_clipboard(&self) -> Option<ClipboardItem> {
424        unsafe {
425            if let Some(text_bytes) = self.read_from_pasteboard(NSPasteboardTypeString) {
426                let text = String::from_utf8_lossy(&text_bytes).to_string();
427                let hash_bytes = self
428                    .read_from_pasteboard(self.text_hash_pasteboard_type)
429                    .and_then(|bytes| bytes.try_into().ok())
430                    .map(u64::from_be_bytes);
431                let metadata_bytes = self
432                    .read_from_pasteboard(self.metadata_pasteboard_type)
433                    .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok());
434
435                if let Some((hash, metadata)) = hash_bytes.zip(metadata_bytes) {
436                    if hash == ClipboardItem::text_hash(&text) {
437                        Some(ClipboardItem {
438                            text,
439                            metadata: Some(metadata),
440                        })
441                    } else {
442                        Some(ClipboardItem {
443                            text,
444                            metadata: None,
445                        })
446                    }
447                } else {
448                    Some(ClipboardItem {
449                        text,
450                        metadata: None,
451                    })
452                }
453            } else {
454                None
455            }
456        }
457    }
458
459    fn open_url(&self, url: &str) {
460        unsafe {
461            let url = NSURL::alloc(nil)
462                .initWithString_(ns_string(url))
463                .autorelease();
464            let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
465            msg_send![workspace, openURL: url]
466        }
467    }
468
469    fn write_credentials(&self, url: &str, username: &str, password: &[u8]) {
470        let url = CFString::from(url);
471        let username = CFString::from(username);
472        let password = CFData::from_buffer(password);
473
474        unsafe {
475            use security::*;
476
477            // First, check if there are already credentials for the given server. If so, then
478            // update the username and password.
479            let mut verb = "updating";
480            let mut query_attrs = CFMutableDictionary::with_capacity(2);
481            query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
482            query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
483
484            let mut attrs = CFMutableDictionary::with_capacity(4);
485            attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
486            attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
487            attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
488            attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
489
490            let mut status = SecItemUpdate(
491                query_attrs.as_concrete_TypeRef(),
492                attrs.as_concrete_TypeRef(),
493            );
494
495            // If there were no existing credentials for the given server, then create them.
496            if status == errSecItemNotFound {
497                verb = "creating";
498                status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
499            }
500
501            if status != errSecSuccess {
502                panic!("{} password failed: {}", verb, status);
503            }
504        }
505    }
506
507    fn read_credentials(&self, url: &str) -> Option<(String, Vec<u8>)> {
508        let url = CFString::from(url);
509        let cf_true = CFBoolean::true_value().as_CFTypeRef();
510
511        unsafe {
512            use security::*;
513
514            // Find any credentials for the given server URL.
515            let mut attrs = CFMutableDictionary::with_capacity(5);
516            attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
517            attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
518            attrs.set(kSecReturnAttributes as *const _, cf_true);
519            attrs.set(kSecReturnData as *const _, cf_true);
520
521            let mut result = CFTypeRef::from(ptr::null_mut());
522            let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
523            match status {
524                security::errSecSuccess => {}
525                security::errSecItemNotFound | security::errSecUserCanceled => return None,
526                _ => panic!("reading password failed: {}", status),
527            }
528
529            let result = CFType::wrap_under_create_rule(result)
530                .downcast::<CFDictionary>()
531                .expect("keychain item was not a dictionary");
532            let username = result
533                .find(kSecAttrAccount as *const _)
534                .expect("account was missing from keychain item");
535            let username = CFType::wrap_under_get_rule(*username)
536                .downcast::<CFString>()
537                .expect("account was not a string");
538            let password = result
539                .find(kSecValueData as *const _)
540                .expect("password was missing from keychain item");
541            let password = CFType::wrap_under_get_rule(*password)
542                .downcast::<CFData>()
543                .expect("password was not a string");
544
545            Some((username.to_string(), password.bytes().to_vec()))
546        }
547    }
548}
549
550unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {
551    let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR);
552    assert!(!platform_ptr.is_null());
553    &*(platform_ptr as *const MacForegroundPlatform)
554}
555
556extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) {
557    unsafe {
558        if let Some(event) = Event::from_native(native_event, None) {
559            let platform = get_foreground_platform(this);
560            if let Some(callback) = platform.0.borrow_mut().event.as_mut() {
561                if callback(event) {
562                    return;
563                }
564            }
565        }
566
567        msg_send![super(this, class!(NSApplication)), sendEvent: native_event]
568    }
569}
570
571extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) {
572    unsafe {
573        let app: id = msg_send![APP_CLASS, sharedApplication];
574        app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
575
576        let platform = get_foreground_platform(this);
577        let callback = platform.0.borrow_mut().finish_launching.take();
578        if let Some(callback) = callback {
579            callback();
580        }
581    }
582}
583
584extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) {
585    let platform = unsafe { get_foreground_platform(this) };
586    if let Some(callback) = platform.0.borrow_mut().become_active.as_mut() {
587        callback();
588    }
589}
590
591extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) {
592    let platform = unsafe { get_foreground_platform(this) };
593    if let Some(callback) = platform.0.borrow_mut().resign_active.as_mut() {
594        callback();
595    }
596}
597
598extern "C" fn open_files(this: &mut Object, _: Sel, _: id, paths: id) {
599    let paths = unsafe {
600        (0..paths.count())
601            .into_iter()
602            .filter_map(|i| {
603                let path = paths.objectAtIndex(i);
604                match CStr::from_ptr(path.UTF8String() as *mut c_char).to_str() {
605                    Ok(string) => Some(PathBuf::from(string)),
606                    Err(err) => {
607                        log::error!("error converting path to string: {}", err);
608                        None
609                    }
610                }
611            })
612            .collect::<Vec<_>>()
613    };
614    let platform = unsafe { get_foreground_platform(this) };
615    if let Some(callback) = platform.0.borrow_mut().open_files.as_mut() {
616        callback(paths);
617    }
618}
619
620extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
621    unsafe {
622        let platform = get_foreground_platform(this);
623        let mut platform = platform.0.borrow_mut();
624        if let Some(mut callback) = platform.menu_command.take() {
625            let tag: NSInteger = msg_send![item, tag];
626            let index = tag as usize;
627            if let Some(action) = platform.menu_actions.get(index) {
628                callback(action.as_ref());
629            }
630            platform.menu_command = Some(callback);
631        }
632    }
633}
634
635unsafe fn ns_string(string: &str) -> id {
636    NSString::alloc(nil).init_str(string).autorelease()
637}
638
639mod security {
640    #![allow(non_upper_case_globals)]
641    use super::*;
642
643    #[link(name = "Security", kind = "framework")]
644    extern "C" {
645        pub static kSecClass: CFStringRef;
646        pub static kSecClassInternetPassword: CFStringRef;
647        pub static kSecAttrServer: CFStringRef;
648        pub static kSecAttrAccount: CFStringRef;
649        pub static kSecValueData: CFStringRef;
650        pub static kSecReturnAttributes: CFStringRef;
651        pub static kSecReturnData: CFStringRef;
652
653        pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
654        pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
655        pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
656    }
657
658    pub const errSecSuccess: OSStatus = 0;
659    pub const errSecUserCanceled: OSStatus = -128;
660    pub const errSecItemNotFound: OSStatus = -25300;
661}
662
663#[cfg(test)]
664mod tests {
665    use crate::platform::Platform;
666
667    use super::*;
668
669    #[test]
670    fn test_clipboard() {
671        let platform = build_platform();
672        assert_eq!(platform.read_from_clipboard(), None);
673
674        let item = ClipboardItem::new("1".to_string());
675        platform.write_to_clipboard(item.clone());
676        assert_eq!(platform.read_from_clipboard(), Some(item));
677
678        let item = ClipboardItem::new("2".to_string()).with_metadata(vec![3, 4]);
679        platform.write_to_clipboard(item.clone());
680        assert_eq!(platform.read_from_clipboard(), Some(item));
681
682        let text_from_other_app = "text from other app";
683        unsafe {
684            let bytes = NSData::dataWithBytes_length_(
685                nil,
686                text_from_other_app.as_ptr() as *const c_void,
687                text_from_other_app.len() as u64,
688            );
689            platform
690                .pasteboard
691                .setData_forType(bytes, NSPasteboardTypeString);
692        }
693        assert_eq!(
694            platform.read_from_clipboard(),
695            Some(ClipboardItem::new(text_from_other_app.to_string()))
696        );
697    }
698
699    fn build_platform() -> MacPlatform {
700        let mut platform = MacPlatform::new();
701        platform.pasteboard = unsafe { NSPasteboard::pasteboardWithUniqueName(nil) };
702        platform
703    }
704}