1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::io;
4use std::ops::{BitOr, ControlFlow};
5use std::sync::{Arc, OnceLock};
6
7use dispatch2::MainThreadBound;
8use objc2::rc::{Retained, Weak};
9use objc2::runtime::AnyObject;
10use objc2::{AnyThread, DefinedClass as _, MainThreadMarker, Message, define_class, msg_send};
11use objc2_app_kit::{
12 NSDragOperation, NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML,
13 NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF,
14 NSPasteboardWriting, NSPasteboardWritingOptions,
15};
16use objc2_foundation::{NSArray, NSData, NSObject, NSObjectProtocol, NSString};
17use winit_core::data_transfer::{
18 DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData,
19};
20use winit_core::event_loop::DndAction;
21use winit_core::window::WindowId;
22
23#[derive(PartialEq, Eq, Debug, Clone)]
25pub struct PasteboardType {
26 hint: Option<TypeHint>,
27 inner: Arc<str>,
29}
30
31impl PasteboardType {
32 fn from_hint(hint: TypeHint) -> Option<Self> {
33 let hint_to_pasteboard_type = unsafe {
34 [
35 (TypeHint::UriList, NSPasteboardTypeFileURL),
36 (TypeHint::Plaintext, NSPasteboardTypeString),
37 (TypeHint::Html, NSPasteboardTypeHTML),
38 (TypeHint::Image { extension_hint: Some("png") }, NSPasteboardTypePNG),
39 (TypeHint::Image { extension_hint: Some("tiff") }, NSPasteboardTypeTIFF),
40 (TypeHint::Audio { extension_hint: None }, NSPasteboardTypeSound),
41 ]
42 };
43
44 hint_to_pasteboard_type.into_iter().find_map(|(haystack, inner)| {
45 (haystack.matches(&hint))
46 .then(|| Self { hint: Some(hint), inner: inner.to_string().into() })
47 })
48 }
49}
50
51impl From<Retained<NSPasteboardType>> for PasteboardType {
52 fn from(value: Retained<NSPasteboardType>) -> Self {
53 let pasteboard_type_to_hint = unsafe {
54 [
55 #[expect(deprecated)]
58 (objc2_app_kit::NSFilenamesPboardType, TypeHint::UriList),
59 (NSPasteboardTypeFileURL, TypeHint::UriList),
60 (NSPasteboardTypeString, TypeHint::Plaintext),
61 (NSPasteboardTypeHTML, TypeHint::Html),
62 (NSPasteboardTypePNG, TypeHint::Image { extension_hint: Some("png") }),
63 (NSPasteboardTypeTIFF, TypeHint::Image { extension_hint: Some("tiff") }),
64 (NSPasteboardTypeSound, TypeHint::Audio { extension_hint: None }),
65 ]
66 };
67
68 let hint = pasteboard_type_to_hint
69 .iter()
70 .find_map(|(pb_type, hint)| (**pb_type == *value).then_some(hint));
71
72 Self { hint: hint.copied(), inner: value.to_string().into() }
73 }
74}
75
76impl TransferType for PasteboardType {
77 fn hint(&self) -> Option<winit_core::data_transfer::TypeHint> {
78 self.hint
79 }
80
81 fn matches(&self, other: &dyn TransferType) -> bool {
82 if let Some(other_pb_type) = other.cast_ref::<Self>() {
83 *self == *other_pb_type
84 } else {
85 self.hint().is_some_and(|hint| other.hint() == Some(hint))
87 }
88 }
89}
90
91#[derive(Debug)]
93pub struct Pasteboard {
94 transfer_id: DataTransferId,
95 ns_pasteboard: MainThreadBound<Retained<NSPasteboard>>,
96 types: OnceLock<Arc<[PasteboardType]>>,
97}
98
99impl Clone for Pasteboard {
100 fn clone(&self) -> Self {
101 let inner = self.ns_pasteboard.get_on_main(|inner| {
102 MainThreadBound::new(inner.clone(), MainThreadMarker::new().unwrap())
103 });
104
105 Self { transfer_id: self.transfer_id, ns_pasteboard: inner, types: self.types.clone() }
106 }
107}
108
109impl Pasteboard {
110 fn new(
111 transfer_id: DataTransferId,
112 ns_pasteboard: MainThreadBound<Retained<NSPasteboard>>,
113 ) -> Self {
114 Self { transfer_id, ns_pasteboard, types: Default::default() }
115 }
116
117 pub fn types(&self) -> &[PasteboardType] {
119 self.types.get_or_init(|| {
120 self.ns_pasteboard.get_on_main(|pb| {
121 pb.types()
122 .map(|types| types.into_iter().map(PasteboardType::from).collect::<Vec<_>>())
123 .unwrap_or_default()
124 .into()
125 })
126 })
127 }
128
129 pub fn id(&self) -> DataTransferId {
131 self.transfer_id
132 }
133
134 pub(crate) fn with_type(&self, type_: PasteboardTypeSpec) -> PasteboardValue {
138 PasteboardValue { type_, pasteboard: self.clone() }
139 }
140}
141
142impl DataTransfer for Pasteboard {
143 fn for_each_available_type<'this>(
144 &'this self,
145 func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>,
146 ) {
147 let _ = self.types().iter().map(|mime| mime as &dyn TransferType).try_for_each(func);
148 }
149}
150
151#[derive(Debug, Clone)]
152pub(crate) enum PasteboardTypeSpec {
153 PasteboardType(PasteboardType),
154 TypeHint(TypeHint),
155}
156
157impl PasteboardTypeSpec {
158 pub(crate) fn from_dyn(type_: &dyn TransferType) -> Option<Self> {
159 match type_.cast_ref::<PasteboardType>() {
160 Some(pb_type) => Some(Self::PasteboardType(pb_type.clone())),
161 None => type_.hint().map(Into::into),
162 }
163 }
164}
165
166impl From<TypeHint> for PasteboardTypeSpec {
167 fn from(value: TypeHint) -> Self {
168 match PasteboardType::from_hint(value) {
169 Some(pb_type) => Self::PasteboardType(pb_type),
170 None => Self::TypeHint(value),
171 }
172 }
173}
174
175impl PasteboardTypeSpec {
176 fn pasteboard_type(&self) -> Option<&PasteboardType> {
177 match self {
178 PasteboardTypeSpec::PasteboardType(pasteboard_type) => Some(pasteboard_type),
179 PasteboardTypeSpec::TypeHint(_) => None,
180 }
181 }
182}
183
184pub fn dnd_action_to_ns_drag_operation(value: DndAction) -> NSDragOperation {
185 match value {
186 DndAction::Copy => NSDragOperation::Copy,
187 DndAction::Move => NSDragOperation::Move,
188 DndAction::Link => NSDragOperation::Link,
189 DndAction::Private => NSDragOperation::Private,
190 _ => NSDragOperation::empty(),
191 }
192}
193
194pub fn ns_drag_operation_to_dnd_action(value: NSDragOperation) -> Option<DndAction> {
195 [
196 (NSDragOperation::Copy, DndAction::Copy),
197 (NSDragOperation::Move, DndAction::Move),
198 (NSDragOperation::Link, DndAction::Link),
199 (NSDragOperation::Private, DndAction::Private),
200 (NSDragOperation::Generic, DndAction::Copy),
202 ]
203 .into_iter()
204 .find_map(|(appkit, winit)| value.contains(appkit).then_some(winit))
205}
206
207pub fn dnd_actions_to_ns_drag_operation(value: &[DndAction]) -> NSDragOperation {
208 value
209 .iter()
210 .copied()
211 .map(dnd_action_to_ns_drag_operation)
212 .fold(NSDragOperation::empty(), BitOr::bitor)
213}
214
215pub fn preferred_drag_operation(
216 value: NSDragOperation,
217 preference: &[DndAction],
218) -> Option<DndAction> {
219 preference
220 .iter()
221 .find(|action| value.intersects(dnd_action_to_ns_drag_operation(**action)))
222 .copied()
223}
224
225#[derive(Debug)]
227pub struct PasteboardValue {
228 type_: PasteboardTypeSpec,
232 pasteboard: Pasteboard,
233}
234
235impl TypedData for PasteboardValue {
236 fn type_(&self) -> &dyn TransferType {
237 match &self.type_ {
238 PasteboardTypeSpec::PasteboardType(pasteboard_type) => {
239 pasteboard_type as &dyn TransferType
240 },
241 PasteboardTypeSpec::TypeHint(type_hint) => type_hint,
242 }
243 }
244
245 fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
246 self.try_as_bytes()
247 .ok()
248 .map(|bytes| Box::new(io::Cursor::new(bytes)) as Box<dyn io::BufRead>)
249 }
250
251 fn try_as_bytes(&self) -> io::Result<Vec<u8>> {
252 let type_ = self.type_.clone();
253 self.pasteboard
254 .ns_pasteboard
255 .get_on_main(|pasteboard| {
256 let bytes =
257 pasteboard.dataForType(&NSString::from_str(&type_.pasteboard_type()?.inner))?;
258 Some(bytes.to_vec())
259 })
260 .ok_or_else(|| {
261 io::Error::other(format!(
262 "NSPasteboard doesn't advertise a binary representation for type {:?}",
263 self.type_
264 ))
265 })
266 }
267
268 fn try_as_uris(&self) -> io::Result<Vec<String>> {
269 if self.type_().hint() != Some(TypeHint::UriList) {
271 return Err(io::ErrorKind::InvalidData.into());
272 }
273
274 self.pasteboard.ns_pasteboard.get_on_main(|pasteboard| {
275 let Some(items) = pasteboard.pasteboardItems() else {
276 #[expect(deprecated)]
278 let property_list = match pasteboard
279 .propertyListForType(unsafe { objc2_app_kit::NSFilenamesPboardType })
280 {
281 Some(property_list) => property_list,
282 None => {
283 return pasteboard
284 .stringForType(unsafe { NSPasteboardTypeFileURL })
285 .map(|ns_str| vec![ns_str.to_string()])
286 .ok_or_else(|| io::ErrorKind::InvalidData.into());
287 },
288 };
289
290 let paths = property_list
291 .downcast::<NSArray>()
292 .unwrap()
293 .into_iter()
294 .map(|file| file.downcast::<NSString>().unwrap().to_string())
295 .collect();
296
297 return Ok(paths);
298 };
299
300 Ok(items
301 .into_iter()
302 .filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL }))
303 .map(|ns_str| ns_str.to_string())
304 .collect())
305 })
306 }
307
308 fn try_as_string(&self) -> io::Result<String> {
309 let type_ = self.type_.clone();
310
311 self.pasteboard.ns_pasteboard.get_on_main(|pasteboard| {
312 pasteboard
313 .stringForType(&NSString::from_str(
314 &type_.pasteboard_type().ok_or(io::ErrorKind::InvalidData)?.inner,
315 ))
316 .map(|ns_str| ns_str.to_string())
317 .ok_or_else(|| io::ErrorKind::InvalidData.into())
318 })
319 }
320}
321
322#[derive(Debug)]
323struct ActivePasteboard {
324 window_id: WindowId,
325 pb: MainThreadBound<Weak<NSPasteboard>>,
326}
327
328#[derive(Debug, Default)]
329pub struct Pasteboards {
330 inner: RefCell<HashMap<DataTransferId, ActivePasteboard>>,
331}
332
333impl Pasteboards {
334 pub fn remove_deloaded_pasteboards(&self) {
335 self.inner.borrow_mut().retain(|_, ActivePasteboard { pb, .. }| {
336 pb.get_on_main(|state| state.load().is_some())
337 });
338 }
339
340 pub fn set_pasteboard(
342 &self,
343 id: DataTransferId,
344 new_pb: &MainThreadBound<Retained<NSPasteboard>>,
345 ) {
346 let mut inner = self.inner.borrow_mut();
347 if let Some(ActivePasteboard { pb, .. }) = inner.get_mut(&id) {
348 *pb = new_pb.get_on_main(|pb| {
349 MainThreadBound::new(Weak::from_retained(pb), MainThreadMarker::new().unwrap())
350 });
351 }
352 }
353
354 pub fn insert(
355 &self,
356 transfer_id: DataTransferId,
357 pb: &MainThreadBound<Retained<NSPasteboard>>,
358 window_id: WindowId,
359 ) {
360 let mut inner = self.inner.borrow_mut();
361 let transfer = inner.entry(transfer_id).or_insert_with(move || {
362 pb.get_on_main(move |pb| ActivePasteboard {
363 window_id,
364 pb: MainThreadBound::new(Weak::from_retained(pb), MainThreadMarker::new().unwrap()),
365 })
366 });
367
368 transfer.window_id = window_id;
369 }
370
371 pub fn get(&self, id: DataTransferId) -> Option<Pasteboard> {
372 self.inner.borrow().get(&id).and_then(|ActivePasteboard { pb, .. }| {
373 pb.get_on_main(|state| {
374 let pb = state.load()?;
375 let pb = MainThreadBound::new(pb, MainThreadMarker::new().unwrap());
376 Some(Pasteboard::new(id, pb))
377 })
378 })
379 }
380
381 pub fn window_id(&self, id: DataTransferId) -> Option<WindowId> {
383 self.inner.borrow().get(&id).map(|active_pasteboard| active_pasteboard.window_id)
384 }
385}
386
387pub(crate) struct PasteboardWriterState {
388 data: Box<dyn DataTransferSend>,
389 uri: Option<Retained<NSString>>,
393 writable_types: Retained<NSArray<NSPasteboardType>>,
394}
395
396impl PasteboardWriter {
397 pub(crate) fn new(
398 value: Box<dyn DataTransferSend>,
399 uri: Option<Retained<NSString>>,
400 ) -> Retained<Self> {
401 let mut writable_types = Vec::<Retained<NSPasteboardType>>::new();
402 value.for_each_available_type(&mut |type_| {
403 let Some(spec) = PasteboardTypeSpec::from_dyn(type_) else {
404 return ControlFlow::Continue(());
405 };
406
407 let Some(pb_type) = spec.pasteboard_type() else {
408 return ControlFlow::Continue(());
409 };
410
411 writable_types.push(NSString::from_str(&pb_type.inner));
412
413 ControlFlow::Continue(())
414 });
415
416 let pb_writer = Self::alloc().set_ivars(PasteboardWriterState {
417 data: value,
418 uri,
419 writable_types: NSArray::from_retained_slice(&writable_types),
420 });
421
422 unsafe { msg_send![super(pb_writer), init] }
424 }
425}
426
427impl PasteboardWriterState {
428 fn data_for_pasteboard_type(
429 &self,
430 pasteboard_type: &NSPasteboardType,
431 ) -> Option<Retained<AnyObject>> {
432 if pasteboard_type == unsafe { NSPasteboardTypeFileURL } {
433 if let Some(out) = self.uri.clone().map(Into::into) {
434 return Some(out);
435 }
436 }
437 let pb_type = PasteboardType::from(pasteboard_type.retain());
438
439 let mut out = None;
440
441 self.data.for_each_available_type(&mut |haystack| {
442 if haystack.matches(&pb_type) {
443 out = self.data.data_for_type(haystack);
444 ControlFlow::Break(())
445 } else {
446 ControlFlow::Continue(())
447 }
448 });
449
450 match out? {
451 SendData::Uris(_) => None,
454 SendData::String(string) => Some(NSString::from_str(&string).into()),
455 SendData::Bytes(binary) => Some(NSData::from_vec(binary).into()),
456 _ => None,
457 }
458 }
459}
460
461define_class!(
462 #[unsafe(super(NSObject))]
463 #[thread_kind = AnyThread]
464 #[name = "WinitPasteboardWriter"]
465 #[ivars = PasteboardWriterState]
466 pub(crate) struct PasteboardWriter;
467
468 unsafe impl NSObjectProtocol for PasteboardWriter {}
469
470 unsafe impl NSPasteboardWriting for PasteboardWriter {
471 #[unsafe(method_id(writableTypesForPasteboard:))]
472 fn writable_types_for_pasteboard(
473 &self,
474 _: &NSPasteboard,
475 ) -> Retained<NSArray<NSPasteboardType>> {
476 let vars = self.ivars();
477 vars.writable_types.clone()
478 }
479
480 #[unsafe(method(writingOptionsForType:pasteboard:))]
481 fn writing_options_for_type(
482 &self,
483 type_: &NSPasteboardType,
484 pasteboard: &NSPasteboard,
485 ) -> NSPasteboardWritingOptions {
486 let _ = type_;
487 let _ = pasteboard;
488 NSPasteboardWritingOptions::empty()
489 }
490
491 #[unsafe(method_id(pasteboardPropertyListForType:))]
492 fn pasteboard_property_list_for_type(
493 &self,
494 type_: &NSPasteboardType,
495 ) -> Option<Retained<AnyObject>> {
496 let vars = self.ivars();
497 vars.data_for_pasteboard_type(type_)
498 }
499 }
500);