objc2-foundation 0.2.0-alpha.6

Bindings to the Objective-C Cocoa Foundation framework
Documentation
//! Read from the global pasteboard, and write a new string into it.
//!
//! Works on macOS 10.7+
#![deny(unsafe_op_in_unsafe_fn)]
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};

use objc2::rc::{Id, Shared};
use objc2::runtime::{Class, Object};
use objc2::{class, msg_send, msg_send_bool, msg_send_id};
use objc2::{Encoding, Message, RefEncode};

use objc2_foundation::{NSArray, NSDictionary, NSInteger, NSObject, NSString};

type NSPasteboardType = NSString;
type NSPasteboardReadingOptionKey = NSString;

#[cfg(all(feature = "apple", target_os = "macos"))]
#[link(name = "AppKit", kind = "framework")]
extern "C" {
    /// <https://developer.apple.com/documentation/appkit/nspasteboardtypestring?language=objc>
    static NSPasteboardTypeString: Option<&'static NSPasteboardType>;
}
#[cfg(not(all(feature = "apple", target_os = "macos")))]
#[allow(non_upper_case_globals)]
const NSPasteboardTypeString: Option<&'static NSPasteboardType> = None;

/// <https://developer.apple.com/documentation/appkit/nspasteboard?language=objc>
#[repr(C)]
pub struct NSPasteboard {
    // Required for proper layout
    inner: NSObject,
}

// SAFETY: NSPasteboard is an FFI-safe struct and is encoded as an object.
unsafe impl RefEncode for NSPasteboard {
    const ENCODING_REF: Encoding<'static> = Encoding::Object;
}

// SAFETY: NSPasteboard can safely be sent messages, and it responds to the
// normal `retain`, `release` and `autorelease` messages.
unsafe impl Message for NSPasteboard {}

impl Deref for NSPasteboard {
    type Target = NSObject;

    #[inline]
    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

impl DerefMut for NSPasteboard {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.inner
    }
}

impl NSPasteboard {
    /// Common convenience method.
    pub fn class() -> &'static Class {
        class!(NSPasteboard)
    }

    /// We return a `Shared` `Id` because `general` can easily be called
    /// again, and it would return the same object, resulting in two aliasing
    /// mutable references if we returned an `Owned` Id.
    ///
    /// Besides, even if we could prevent this, there might be Objective-C
    /// code somewhere else that accesses this instance.
    ///
    /// TODO: Is this safe to call outside the main thread?
    ///
    /// <https://developer.apple.com/documentation/appkit/nspasteboard/1530091-generalpasteboard?language=objc>
    pub fn general() -> Id<Self, Shared> {
        unsafe { msg_send_id![Self::class(), generalPasteboard].unwrap() }
    }

    /// Simple, straightforward implementation
    ///
    /// <https://developer.apple.com/documentation/appkit/nspasteboard/1533566-stringfortype?language=objc>
    pub fn text_impl_1(&self) -> Id<NSString, Shared> {
        unsafe { msg_send_id![self, stringForType: NSPasteboardTypeString.unwrap()].unwrap() }
    }

    /// More complex implementation using `readObjectsForClasses:options:`,
    /// intended to show some how some patterns might require more knowledge
    /// of nitty-gritty details.
    ///
    /// <https://developer.apple.com/documentation/appkit/nspasteboard/1524454-readobjectsforclasses?language=objc>
    pub fn text_impl_2(&self) -> Id<NSString, Shared> {
        // The NSPasteboard API is a bit weird, it requires you to pass
        // classes as objects, which `objc2_foundation::NSArray` was not
        // really made for - so we convert the class to an `Object` type
        // instead. Also, we wrap it in `ManuallyDrop` because I'm not sure
        // how classes handle `release` calls?
        //
        // TODO: Investigate and find a better way to express this in objc2.
        let string_classes: ManuallyDrop<[Id<Object, Shared>; 1]> = {
            let cls: *const Class = NSString::class();
            let cls = cls as *mut Object;
            unsafe { ManuallyDrop::new([Id::new(cls).unwrap()]) }
        };
        // Temporary, see https://github.com/rust-lang/rust-clippy/issues/9101
        #[allow(unknown_lints, clippy::explicit_auto_deref)]
        let class_array = NSArray::from_slice(&*string_classes);
        let options = NSDictionary::new();
        let objects = unsafe { self.read_objects_for_classes(&class_array, &options) };

        // TODO: Should perhaps return Id<Object, Shared>?
        let ptr: *const Object = objects.first().unwrap();

        // And this part is weird as well, since we now have to convert the
        // object into an NSString, which we know it to be since that's what
        // we told `readObjectsForClasses:options:`.
        let ptr = ptr as *mut NSString;
        unsafe { Id::retain(ptr) }.unwrap()
    }

    /// Defined here to make it easier to declare which types are expected.
    /// This is a common pattern that I can wholeheartedly recommend!
    ///
    /// SAFETY: `class_array` must contain classes!
    unsafe fn read_objects_for_classes(
        &self,
        class_array: &NSArray<Object, Shared>,
        options: &NSDictionary<NSPasteboardReadingOptionKey, Object>,
    ) -> Id<NSArray<Object, Shared>, Shared> {
        unsafe { msg_send_id![self, readObjectsForClasses: class_array, options: options].unwrap() }
    }

    /// This takes `&self` even though `writeObjects:` would seem to mutate
    /// the pasteboard. "What is going on?", you might rightfully ask!
    ///
    /// We do this because we can't soundly get a mutable reference to the
    /// global `NSPasteboard` instance, see [`NSPasteboard::general`].
    ///
    /// This is sound because `NSPasteboard` contains `NSObject`, which in
    /// turn contains `UnsafeCell`, allowing interior mutability.
    ///
    /// <https://developer.apple.com/documentation/appkit/nspasteboard/1533599-clearcontents?language=objc>
    /// <https://developer.apple.com/documentation/appkit/nspasteboard/1525945-writeobjects?language=objc>
    pub fn set_text(&self, text: Id<NSString, Shared>) {
        let _: NSInteger = unsafe { msg_send![self, clearContents] };
        let string_array = NSArray::from_slice(&[text]);
        if !unsafe { msg_send_bool![self, writeObjects: &*string_array] } {
            panic!("Failed writing to pasteboard");
        }
    }
}

fn main() {
    let pasteboard = NSPasteboard::general();
    let impl_1 = pasteboard.text_impl_1();
    let impl_2 = pasteboard.text_impl_1();
    println!("Pasteboard text from implementation 1 was: {:?}", impl_1);
    println!("Pasteboard text from implementation 2 was: {:?}", impl_2);
    assert_eq!(impl_1, impl_2);

    let s = NSString::from_str("Hello, world!");
    pasteboard.set_text(s.clone());
    println!("Now the pasteboard text should be: {:?}", s);
    assert_eq!(s, pasteboard.text_impl_1());
}