1use std::fmt;
6
7use objc2::rc::Retained;
8use objc2::runtime::ProtocolObject;
9use objc2::{DefinedClass, MainThreadMarker, MainThreadOnly, Message, define_class, msg_send};
10use objc2_app_kit::{
11 NSDragOperation, NSDraggingInfo, NSPasteboardItem, NSPasteboardTypeFileURL,
12 NSPasteboardTypeURL, NSView,
13};
14use objc2_foundation::{NSArray, NSString, NSURL};
15
16use nifty_runtime::DropHandler;
17
18use crate::areas;
19
20pub(crate) struct DropAreaIvars {
21 on_drop: DropHandler,
22}
23
24impl fmt::Debug for DropAreaIvars {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 f.debug_struct("DropAreaIvars").finish_non_exhaustive()
27 }
28}
29
30define_class!(
31 #[unsafe(super = NSView)]
33 #[thread_kind = MainThreadOnly]
34 #[name = "NiftyDropArea"]
35 #[ivars = DropAreaIvars]
36 #[derive(Debug)]
37 pub(crate) struct DropArea;
38);
39
40impl DropArea {
41 pub(crate) fn new(mtm: MainThreadMarker, on_drop: DropHandler) -> Retained<Self> {
43 let this = Self::alloc(mtm).set_ivars(DropAreaIvars { on_drop });
44 areas::hidden(unsafe { msg_send![super(this), init] })
46 }
47}
48
49pub(crate) fn accept_drops(host: &NSView) {
51 host.registerForDraggedTypes(&dropped_types());
52}
53
54pub(crate) fn drag_operation(
57 view: &NSView,
58 drag: &ProtocolObject<dyn NSDraggingInfo>,
59) -> NSDragOperation {
60 let carries_drops = drag
61 .draggingPasteboard()
62 .availableTypeFromArray(&dropped_types())
63 .is_some();
64 if carries_drops && area_under(view, drag).is_some() {
65 NSDragOperation::Copy
66 } else {
67 NSDragOperation::None
68 }
69}
70
71pub(crate) fn perform_drop(view: &NSView, drag: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
74 let Some(area) = area_under(view, drag) else {
75 return false;
76 };
77 let items: Vec<String> = drag
78 .draggingPasteboard()
79 .pasteboardItems()
80 .map(|items| items.iter().filter_map(|item| dropped(&item)).collect())
81 .unwrap_or_default();
82 if items.is_empty() {
83 return false;
84 }
85 (area.ivars().on_drop)(items);
86 true
87}
88
89fn area_under(
92 view: &NSView,
93 drag: &ProtocolObject<dyn NSDraggingInfo>,
94) -> Option<Retained<DropArea>> {
95 let location = drag.draggingLocation();
96 let mut host = Some(view.retain());
97 while let Some(current) = host {
98 let point = current.convertPoint_fromView(location, None);
99 if let Some(area) = areas::innermost::<DropArea>(¤t, point) {
100 return Some(area);
101 }
102 host = unsafe { current.superview() };
104 }
105 None
106}
107
108fn dropped(item: &NSPasteboardItem) -> Option<String> {
110 let (file_url, url) = unsafe { (NSPasteboardTypeFileURL, NSPasteboardTypeURL) };
112 if let Some(file) = item.stringForType(file_url) {
113 return NSURL::URLWithString(&file)
114 .and_then(|file| file.path())
115 .map(|path| path.to_string());
116 }
117 item.stringForType(url).map(|link| link.to_string())
118}
119
120fn dropped_types() -> Retained<NSArray<NSString>> {
122 unsafe { NSArray::from_slice(&[NSPasteboardTypeFileURL, NSPasteboardTypeURL]) }
124}