Skip to main content

max / audiofiles

5.4 KB · 160 lines History Blame Raw
1 //! macOS drag backend: initiates an NSDraggingSession via NSView.
2 //!
3 //! The drag must be deferred with `dispatch_async` because egui's render pass
4 //! runs inside `drawRect:`, where AppKit cannot start a drag session.
5 //! Dispatching to the main queue moves the call to the next runloop iteration.
6 //!
7 //! The `DRAG_ACTIVE` guard (in the parent module) stays set for the entire
8 //! lifetime of the drag session, cleared only in the `draggingSession:endedAtPoint:`
9 //! callback. This prevents re-entrant `beginDraggingSessionWithItems` calls
10 //! (which return NULL and panic in objc2).
11
12 use std::ffi::c_void;
13 use std::path::PathBuf;
14 use std::sync::atomic::Ordering;
15
16 use block2::RcBlock;
17 use objc2::rc::Retained;
18 use objc2::runtime::{NSObject, NSObjectProtocol, ProtocolObject};
19 use objc2::{define_class, AnyThread, MainThreadMarker, MainThreadOnly};
20 use objc2_app_kit::{
21 NSApplication, NSDragOperation, NSDraggingContext, NSDraggingItem, NSDraggingSession,
22 NSDraggingSource, NSEvent, NSEventModifierFlags, NSEventType,
23 };
24 use objc2_foundation::{NSArray, NSPoint, NSRect, NSSize, NSURL};
25 use tracing::{debug, warn};
26
27 unsafe extern "C" {
28 static _dispatch_main_q: c_void;
29 fn dispatch_async(queue: *const c_void, block: &block2::Block<dyn Fn()>);
30 }
31
32 // ---------- Drag source ----------
33
34 define_class!(
35 #[unsafe(super(NSObject))]
36 #[thread_kind = MainThreadOnly]
37 #[name = "AFDragSource"]
38 struct DragSource;
39
40 unsafe impl NSObjectProtocol for DragSource {}
41
42 unsafe impl NSDraggingSource for DragSource {
43 #[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))]
44 fn _source_op_mask(
45 &self,
46 _session: &NSDraggingSession,
47 _context: NSDraggingContext,
48 ) -> NSDragOperation {
49 NSDragOperation::Copy
50 }
51
52 #[unsafe(method(draggingSession:endedAtPoint:operation:))]
53 fn _session_ended(
54 &self,
55 _session: &NSDraggingSession,
56 _screen_point: NSPoint,
57 _operation: NSDragOperation,
58 ) {
59 debug!("Drag session ended");
60 super::DRAG_ACTIVE.store(false, Ordering::Release);
61 }
62 }
63 );
64
65 impl DragSource {
66 fn new(mtm: MainThreadMarker) -> Retained<Self> {
67 // SAFETY: `alloc` + `init` is the standard NSObject construction pattern.
68 // `MainThreadMarker` guarantees we're on the main thread, which `define_class!`
69 // requires for `MainThreadOnly` types. `set_ivars(())` is infallible (no ivars).
70 unsafe { objc2::msg_send![super(Self::alloc(mtm).set_ivars(())), init] }
71 }
72 }
73
74 // ---------- Deferred drag execution ----------
75
76 /// Called on the main queue outside `drawRect:`.
77 /// On failure, clears the DRAG_ACTIVE guard directly; on success, the
78 /// `_session_ended` callback clears it when the drag session finishes.
79 fn do_begin_drag(paths: &[PathBuf]) {
80 if !try_begin_drag(paths) {
81 super::DRAG_ACTIVE.store(false, Ordering::Release);
82 }
83 }
84
85 fn try_begin_drag(paths: &[PathBuf]) -> bool {
86 let Some(mtm) = MainThreadMarker::new() else {
87 warn!("do_begin_drag: not on main thread");
88 return false;
89 };
90
91 let app = NSApplication::sharedApplication(mtm);
92 let Some(window) = app.keyWindow() else {
93 warn!("do_begin_drag: no key window");
94 return false;
95 };
96 let Some(view) = window.contentView() else {
97 warn!("do_begin_drag: no content view");
98 return false;
99 };
100
101 let location = window.mouseLocationOutsideOfEventStream();
102 let Some(event) = NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure(
103 NSEventType::LeftMouseDragged,
104 location,
105 NSEventModifierFlags(0),
106 0.0,
107 window.windowNumber(),
108 None,
109 0,
110 1,
111 1.0,
112 ) else {
113 warn!("do_begin_drag: failed to create synthetic event");
114 return false;
115 };
116
117 let items: Vec<Retained<NSDraggingItem>> = paths
118 .iter()
119 .filter_map(|path| {
120 let url = NSURL::from_file_path(path)?;
121 let writer: &ProtocolObject<dyn objc2_app_kit::NSPasteboardWriting> =
122 ProtocolObject::from_ref(&*url);
123 let item = NSDraggingItem::initWithPasteboardWriter(NSDraggingItem::alloc(), writer);
124 item.setDraggingFrame(NSRect::new(location, NSSize::new(32.0, 32.0)));
125 Some(item)
126 })
127 .collect();
128
129 if items.is_empty() {
130 warn!("do_begin_drag: no dragging items");
131 return false;
132 }
133
134 let items_ref: Vec<&NSDraggingItem> = items.iter().map(|i| &**i).collect();
135 let array = NSArray::from_slice(&items_ref);
136 let source = DragSource::new(mtm);
137 let source_proto: &ProtocolObject<dyn NSDraggingSource> = ProtocolObject::from_ref(&*source);
138
139 debug!(count = items.len(), "Starting NSDraggingSession");
140 let _session = view.beginDraggingSessionWithItems_event_source(&array, &event, source_proto);
141 true
142 }
143
144 // ---------- Public entry point ----------
145
146 pub(super) fn begin_drag_session(paths: &[PathBuf]) -> bool {
147 let paths = paths.to_vec();
148
149 let block = RcBlock::new(move || {
150 do_begin_drag(&paths);
151 });
152 // SAFETY: `_dispatch_main_q` is a valid process-global symbol provided by libdispatch.
153 // `RcBlock` ensures the closure outlives the async dispatch (prevent use-after-free).
154 unsafe {
155 dispatch_async(&_dispatch_main_q, &block);
156 }
157
158 true
159 }
160