use std::cell::RefCell; use std::collections::HashMap; use std::io; use std::ops::{BitOr, ControlFlow}; use std::sync::{Arc, OnceLock}; use dispatch2::MainThreadBound; use objc2::rc::{Retained, Weak}; use objc2::runtime::AnyObject; use objc2::{AnyThread, DefinedClass as _, MainThreadMarker, Message, define_class, msg_send}; use objc2_app_kit::{ NSDragOperation, NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, NSPasteboardWriting, NSPasteboardWritingOptions, }; use objc2_foundation::{NSArray, NSData, NSObject, NSObjectProtocol, NSString}; use winit_core::data_transfer::{ DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, }; use winit_core::event_loop::DndAction; use winit_core::window::WindowId; /// A thin wrapper around [`NSPasteboardType`], implementing [`TransferType`]. #[derive(PartialEq, Eq, Debug, Clone)] pub struct PasteboardType { hint: Option, // We need to convert `NSString` to `str` since `NSString` isn't `Send`/`Sync` inner: Arc, } impl PasteboardType { fn from_hint(hint: TypeHint) -> Option { let hint_to_pasteboard_type = unsafe { [ (TypeHint::UriList, NSPasteboardTypeFileURL), (TypeHint::Plaintext, NSPasteboardTypeString), (TypeHint::Html, NSPasteboardTypeHTML), (TypeHint::Image { extension_hint: Some("png") }, NSPasteboardTypePNG), (TypeHint::Image { extension_hint: Some("tiff") }, NSPasteboardTypeTIFF), (TypeHint::Audio { extension_hint: None }, NSPasteboardTypeSound), ] }; hint_to_pasteboard_type.into_iter().find_map(|(haystack, inner)| { (haystack.matches(&hint)) .then(|| Self { hint: Some(hint), inner: inner.to_string().into() }) }) } } impl From> for PasteboardType { fn from(value: Retained) -> Self { let pasteboard_type_to_hint = unsafe { [ // Just in case the source application uses the deprecated method, we handle it // here #[expect(deprecated)] (objc2_app_kit::NSFilenamesPboardType, TypeHint::UriList), (NSPasteboardTypeFileURL, TypeHint::UriList), (NSPasteboardTypeString, TypeHint::Plaintext), (NSPasteboardTypeHTML, TypeHint::Html), (NSPasteboardTypePNG, TypeHint::Image { extension_hint: Some("png") }), (NSPasteboardTypeTIFF, TypeHint::Image { extension_hint: Some("tiff") }), (NSPasteboardTypeSound, TypeHint::Audio { extension_hint: None }), ] }; let hint = pasteboard_type_to_hint .iter() .find_map(|(pb_type, hint)| (**pb_type == *value).then_some(hint)); Self { hint: hint.copied(), inner: value.to_string().into() } } } impl TransferType for PasteboardType { fn hint(&self) -> Option { self.hint } fn matches(&self, other: &dyn TransferType) -> bool { if let Some(other_pb_type) = other.cast_ref::() { *self == *other_pb_type } else { // If either hint is `None`, return false self.hint().is_some_and(|hint| other.hint() == Some(hint)) } } } /// A thin wrapper around [`NSPasteboard`], implementing [`DataTransfer`]. #[derive(Debug)] pub struct Pasteboard { transfer_id: DataTransferId, ns_pasteboard: MainThreadBound>, types: OnceLock>, } impl Clone for Pasteboard { fn clone(&self) -> Self { let inner = self.ns_pasteboard.get_on_main(|inner| { MainThreadBound::new(inner.clone(), MainThreadMarker::new().unwrap()) }); Self { transfer_id: self.transfer_id, ns_pasteboard: inner, types: self.types.clone() } } } impl Pasteboard { fn new( transfer_id: DataTransferId, ns_pasteboard: MainThreadBound>, ) -> Self { Self { transfer_id, ns_pasteboard, types: Default::default() } } /// Get the array of [`PasteboardType`]s advertized by this [`Pasteboard`]. pub fn types(&self) -> &[PasteboardType] { self.types.get_or_init(|| { self.ns_pasteboard.get_on_main(|pb| { pb.types() .map(|types| types.into_iter().map(PasteboardType::from).collect::>()) .unwrap_or_default() .into() }) }) } /// Get the `DataTransferId` of this pasteboard. pub fn id(&self) -> DataTransferId { self.transfer_id } /// Get a typed reader for this pasteboard. This is only necessary in the cross-platform case, /// as a user downcasting to the platform-specific type can just access the `NSPasteboard` /// directly. pub(crate) fn with_type(&self, type_: PasteboardTypeSpec) -> PasteboardValue { PasteboardValue { type_, pasteboard: self.clone() } } } impl DataTransfer for Pasteboard { fn for_each_available_type<'this>( &'this self, func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, ) { let _ = self.types().iter().map(|mime| mime as &dyn TransferType).try_for_each(func); } } #[derive(Debug, Clone)] pub(crate) enum PasteboardTypeSpec { PasteboardType(PasteboardType), TypeHint(TypeHint), } impl PasteboardTypeSpec { pub(crate) fn from_dyn(type_: &dyn TransferType) -> Option { match type_.cast_ref::() { Some(pb_type) => Some(Self::PasteboardType(pb_type.clone())), None => type_.hint().map(Into::into), } } } impl From for PasteboardTypeSpec { fn from(value: TypeHint) -> Self { match PasteboardType::from_hint(value) { Some(pb_type) => Self::PasteboardType(pb_type), None => Self::TypeHint(value), } } } impl PasteboardTypeSpec { fn pasteboard_type(&self) -> Option<&PasteboardType> { match self { PasteboardTypeSpec::PasteboardType(pasteboard_type) => Some(pasteboard_type), PasteboardTypeSpec::TypeHint(_) => None, } } } pub fn dnd_action_to_ns_drag_operation(value: DndAction) -> NSDragOperation { match value { DndAction::Copy => NSDragOperation::Copy, DndAction::Move => NSDragOperation::Move, DndAction::Link => NSDragOperation::Link, DndAction::Private => NSDragOperation::Private, _ => NSDragOperation::empty(), } } pub fn ns_drag_operation_to_dnd_action(value: NSDragOperation) -> Option { [ (NSDragOperation::Copy, DndAction::Copy), (NSDragOperation::Move, DndAction::Move), (NSDragOperation::Link, DndAction::Link), (NSDragOperation::Private, DndAction::Private), // Sometimes the OS returns `Generic`, in which case we just fall back to `Copy`. (NSDragOperation::Generic, DndAction::Copy), ] .into_iter() .find_map(|(appkit, winit)| value.contains(appkit).then_some(winit)) } pub fn dnd_actions_to_ns_drag_operation(value: &[DndAction]) -> NSDragOperation { value .iter() .copied() .map(dnd_action_to_ns_drag_operation) .fold(NSDragOperation::empty(), BitOr::bitor) } pub fn preferred_drag_operation( value: NSDragOperation, preference: &[DndAction], ) -> Option { preference .iter() .find(|action| value.intersects(dnd_action_to_ns_drag_operation(**action))) .copied() } /// A thin wrapper around [`NSPasteboard`], implementing [`TypedData`]. #[derive(Debug)] pub struct PasteboardValue { // The concept of "top-level" types for a pasteboard doesn't always make sense on macOS due to // the use of `pasteboardItems`, so we allow using `TypeHint` instead to preserve the user's // intention. type_: PasteboardTypeSpec, pasteboard: Pasteboard, } impl TypedData for PasteboardValue { fn type_(&self) -> &dyn TransferType { match &self.type_ { PasteboardTypeSpec::PasteboardType(pasteboard_type) => { pasteboard_type as &dyn TransferType }, PasteboardTypeSpec::TypeHint(type_hint) => type_hint, } } fn try_read(&self) -> Option> { self.try_as_bytes() .ok() .map(|bytes| Box::new(io::Cursor::new(bytes)) as Box) } fn try_as_bytes(&self) -> io::Result> { let type_ = self.type_.clone(); self.pasteboard .ns_pasteboard .get_on_main(|pasteboard| { let bytes = pasteboard.dataForType(&NSString::from_str(&type_.pasteboard_type()?.inner))?; Some(bytes.to_vec()) }) .ok_or_else(|| { io::Error::other(format!( "NSPasteboard doesn't advertise a binary representation for type {:?}", self.type_ )) }) } fn try_as_uris(&self) -> io::Result> { // TODO: We should probably use `readObjects`, need to check how that works. if self.type_().hint() != Some(TypeHint::UriList) { return Err(io::ErrorKind::InvalidData.into()); } self.pasteboard.ns_pasteboard.get_on_main(|pasteboard| { let Some(items) = pasteboard.pasteboardItems() else { // The pasteboard didn't expose any items, so we try with the deprecated method. #[expect(deprecated)] let property_list = match pasteboard .propertyListForType(unsafe { objc2_app_kit::NSFilenamesPboardType }) { Some(property_list) => property_list, None => { return pasteboard .stringForType(unsafe { NSPasteboardTypeFileURL }) .map(|ns_str| vec![ns_str.to_string()]) .ok_or_else(|| io::ErrorKind::InvalidData.into()); }, }; let paths = property_list .downcast::() .unwrap() .into_iter() .map(|file| file.downcast::().unwrap().to_string()) .collect(); return Ok(paths); }; Ok(items .into_iter() .filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL })) .map(|ns_str| ns_str.to_string()) .collect()) }) } fn try_as_string(&self) -> io::Result { let type_ = self.type_.clone(); self.pasteboard.ns_pasteboard.get_on_main(|pasteboard| { pasteboard .stringForType(&NSString::from_str( &type_.pasteboard_type().ok_or(io::ErrorKind::InvalidData)?.inner, )) .map(|ns_str| ns_str.to_string()) .ok_or_else(|| io::ErrorKind::InvalidData.into()) }) } } #[derive(Debug)] struct ActivePasteboard { window_id: WindowId, pb: MainThreadBound>, } #[derive(Debug, Default)] pub struct Pasteboards { inner: RefCell>, } impl Pasteboards { pub fn remove_deloaded_pasteboards(&self) { self.inner.borrow_mut().retain(|_, ActivePasteboard { pb, .. }| { pb.get_on_main(|state| state.load().is_some()) }); } /// If the data transfer exists, update the pasteboard it points to. pub fn set_pasteboard( &self, id: DataTransferId, new_pb: &MainThreadBound>, ) { let mut inner = self.inner.borrow_mut(); if let Some(ActivePasteboard { pb, .. }) = inner.get_mut(&id) { *pb = new_pb.get_on_main(|pb| { MainThreadBound::new(Weak::from_retained(pb), MainThreadMarker::new().unwrap()) }); } } pub fn insert( &self, transfer_id: DataTransferId, pb: &MainThreadBound>, window_id: WindowId, ) { let mut inner = self.inner.borrow_mut(); let transfer = inner.entry(transfer_id).or_insert_with(move || { pb.get_on_main(move |pb| ActivePasteboard { window_id, pb: MainThreadBound::new(Weak::from_retained(pb), MainThreadMarker::new().unwrap()), }) }); transfer.window_id = window_id; } pub fn get(&self, id: DataTransferId) -> Option { self.inner.borrow().get(&id).and_then(|ActivePasteboard { pb, .. }| { pb.get_on_main(|state| { let pb = state.load()?; let pb = MainThreadBound::new(pb, MainThreadMarker::new().unwrap()); Some(Pasteboard::new(id, pb)) }) }) } /// Get the window ID that most-recently saw the provided data transfer. pub fn window_id(&self, id: DataTransferId) -> Option { self.inner.borrow().get(&id).map(|active_pasteboard| active_pasteboard.window_id) } } pub(crate) struct PasteboardWriterState { data: Box, // The macOS drag-and-drop API has some confusing aspects when handling multi-drag. The best // we can really do is have the first element contain all the cross-platform items, and // any further items are file paths only. uri: Option>, writable_types: Retained>, } impl PasteboardWriter { pub(crate) fn new( value: Box, uri: Option>, ) -> Retained { let mut writable_types = Vec::>::new(); value.for_each_available_type(&mut |type_| { let Some(spec) = PasteboardTypeSpec::from_dyn(type_) else { return ControlFlow::Continue(()); }; let Some(pb_type) = spec.pasteboard_type() else { return ControlFlow::Continue(()); }; writable_types.push(NSString::from_str(&pb_type.inner)); ControlFlow::Continue(()) }); let pb_writer = Self::alloc().set_ivars(PasteboardWriterState { data: value, uri, writable_types: NSArray::from_retained_slice(&writable_types), }); // Unsure if there's an easier way to do this, but this is how `WindowDelegate` does it. unsafe { msg_send![super(pb_writer), init] } } } impl PasteboardWriterState { fn data_for_pasteboard_type( &self, pasteboard_type: &NSPasteboardType, ) -> Option> { if pasteboard_type == unsafe { NSPasteboardTypeFileURL } { if let Some(out) = self.uri.clone().map(Into::into) { return Some(out); } } let pb_type = PasteboardType::from(pasteboard_type.retain()); let mut out = None; self.data.for_each_available_type(&mut |haystack| { if haystack.matches(&pb_type) { out = self.data.data_for_type(haystack); ControlFlow::Break(()) } else { ControlFlow::Continue(()) } }); match out? { // This should be handled separately // TODO: Is there a better way to do this? SendData::Uris(_) => None, SendData::String(string) => Some(NSString::from_str(&string).into()), SendData::Bytes(binary) => Some(NSData::from_vec(binary).into()), _ => None, } } } define_class!( #[unsafe(super(NSObject))] #[thread_kind = AnyThread] #[name = "WinitPasteboardWriter"] #[ivars = PasteboardWriterState] pub(crate) struct PasteboardWriter; unsafe impl NSObjectProtocol for PasteboardWriter {} unsafe impl NSPasteboardWriting for PasteboardWriter { #[unsafe(method_id(writableTypesForPasteboard:))] fn writable_types_for_pasteboard( &self, _: &NSPasteboard, ) -> Retained> { let vars = self.ivars(); vars.writable_types.clone() } #[unsafe(method(writingOptionsForType:pasteboard:))] fn writing_options_for_type( &self, type_: &NSPasteboardType, pasteboard: &NSPasteboard, ) -> NSPasteboardWritingOptions { let _ = type_; let _ = pasteboard; NSPasteboardWritingOptions::empty() } #[unsafe(method_id(pasteboardPropertyListForType:))] fn pasteboard_property_list_for_type( &self, type_: &NSPasteboardType, ) -> Option> { let vars = self.ivars(); vars.data_for_pasteboard_type(type_) } } );