Skip to main content

nifty_macos/
drop.rs

1//! Drop targets: a hidden view that marks the area a stack's drop target covers, and the
2//! dragging-destination work of the views widgets sit in, which take files and links once
3//! one of their stacks has a drop target.
4
5use 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    // SAFETY: NSView has no subclassing requirements and `DropArea` does not implement Drop.
32    #[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    /// An area that gives what is dropped on it to `on_drop`.
42    pub(crate) fn new(mtm: MainThreadMarker, on_drop: DropHandler) -> Retained<Self> {
43        let this = Self::alloc(mtm).set_ivars(DropAreaIvars { on_drop });
44        // SAFETY: `init` is NSView's designated initializer and takes no arguments.
45        areas::hidden(unsafe { msg_send![super(this), init] })
46    }
47}
48
49/// Lets `host`, a view widgets sit in, take drags of files and links.
50pub(crate) fn accept_drops(host: &NSView) {
51    host.registerForDraggedTypes(&dropped_types());
52}
53
54/// What a drag over `view` would do: copy, when a drop area is under it and it carries
55/// files or links.
56pub(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
71/// Gives the files and links of a drag dropped on `view` to the drop area under it,
72/// answering whether one took them.
73pub(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
89/// The drop area under the drag in `view` or the views around it, nearest first, since a
90/// scroll area's content sits in a view of its own inside the stack around it.
91fn 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>(&current, point) {
100            return Some(area);
101        }
102        // SAFETY: views are only moved between superviews on the main thread, which this is.
103        host = unsafe { current.superview() };
104    }
105    None
106}
107
108/// A file's path, or a link's address.
109fn dropped(item: &NSPasteboardItem) -> Option<String> {
110    // SAFETY: AppKit defines the pasteboard types as immutable constants.
111    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
120/// The pasteboard types a drop target takes: files, and links.
121fn dropped_types() -> Retained<NSArray<NSString>> {
122    // SAFETY: AppKit defines the pasteboard types as immutable constants.
123    unsafe { NSArray::from_slice(&[NSPasteboardTypeFileURL, NSPasteboardTypeURL]) }
124}