Skip to main content

max / audiofiles

5.6 KB · 163 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 // SAFETY: `_dispatch_main_q` and `dispatch_async` are public symbols exported by
28 // Apple's libdispatch. The declared signatures match the system headers; dispatch
29 // is thread-safe and `RcBlock` keeps the closure alive across the async boundary.
30 unsafe extern "C" {
31 static _dispatch_main_q: c_void;
32 fn dispatch_async(queue: *const c_void, block: &block2::Block<dyn Fn()>);
33 }
34
35 // ---------- Drag source ----------
36
37 define_class!(
38 #[unsafe(super(NSObject))]
39 #[thread_kind = MainThreadOnly]
40 #[name = "AFDragSource"]
41 struct DragSource;
42
43 unsafe impl NSObjectProtocol for DragSource {}
44
45 unsafe impl NSDraggingSource for DragSource {
46 #[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))]
47 fn _source_op_mask(
48 &self,
49 _session: &NSDraggingSession,
50 _context: NSDraggingContext,
51 ) -> NSDragOperation {
52 NSDragOperation::Copy
53 }
54
55 #[unsafe(method(draggingSession:endedAtPoint:operation:))]
56 fn _session_ended(
57 &self,
58 _session: &NSDraggingSession,
59 _screen_point: NSPoint,
60 _operation: NSDragOperation,
61 ) {
62 debug!("Drag session ended");
63 super::DRAG_ACTIVE.store(false, Ordering::Release);
64 }
65 }
66 );
67
68 impl DragSource {
69 fn new(mtm: MainThreadMarker) -> Retained<Self> {
70 // SAFETY: `alloc` + `init` is the standard NSObject construction pattern.
71 // `MainThreadMarker` guarantees we're on the main thread, which `define_class!`
72 // requires for `MainThreadOnly` types. `set_ivars(())` is infallible (no ivars).
73 unsafe { objc2::msg_send![super(Self::alloc(mtm).set_ivars(())), init] }
74 }
75 }
76
77 // ---------- Deferred drag execution ----------
78
79 /// Called on the main queue outside `drawRect:`.
80 /// On failure, clears the DRAG_ACTIVE guard directly; on success, the
81 /// `_session_ended` callback clears it when the drag session finishes.
82 fn do_begin_drag(paths: &[PathBuf]) {
83 if !try_begin_drag(paths) {
84 super::DRAG_ACTIVE.store(false, Ordering::Release);
85 }
86 }
87
88 fn try_begin_drag(paths: &[PathBuf]) -> bool {
89 let Some(mtm) = MainThreadMarker::new() else {
90 warn!("do_begin_drag: not on main thread");
91 return false;
92 };
93
94 let app = NSApplication::sharedApplication(mtm);
95 let Some(window) = app.keyWindow() else {
96 warn!("do_begin_drag: no key window");
97 return false;
98 };
99 let Some(view) = window.contentView() else {
100 warn!("do_begin_drag: no content view");
101 return false;
102 };
103
104 let location = window.mouseLocationOutsideOfEventStream();
105 let Some(event) = NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure(
106 NSEventType::LeftMouseDragged,
107 location,
108 NSEventModifierFlags(0),
109 0.0,
110 window.windowNumber(),
111 None,
112 0,
113 1,
114 1.0,
115 ) else {
116 warn!("do_begin_drag: failed to create synthetic event");
117 return false;
118 };
119
120 let items: Vec<Retained<NSDraggingItem>> = paths
121 .iter()
122 .filter_map(|path| {
123 let url = NSURL::from_file_path(path)?;
124 let writer: &ProtocolObject<dyn objc2_app_kit::NSPasteboardWriting> =
125 ProtocolObject::from_ref(&*url);
126 let item = NSDraggingItem::initWithPasteboardWriter(NSDraggingItem::alloc(), writer);
127 item.setDraggingFrame(NSRect::new(location, NSSize::new(32.0, 32.0)));
128 Some(item)
129 })
130 .collect();
131
132 if items.is_empty() {
133 warn!("do_begin_drag: no dragging items");
134 return false;
135 }
136
137 let items_ref: Vec<&NSDraggingItem> = items.iter().map(|i| &**i).collect();
138 let array = NSArray::from_slice(&items_ref);
139 let source = DragSource::new(mtm);
140 let source_proto: &ProtocolObject<dyn NSDraggingSource> = ProtocolObject::from_ref(&*source);
141
142 debug!(count = items.len(), "Starting NSDraggingSession");
143 let _session = view.beginDraggingSessionWithItems_event_source(&array, &event, source_proto);
144 true
145 }
146
147 // ---------- Public entry point ----------
148
149 pub(super) fn begin_drag_session(paths: &[PathBuf]) -> bool {
150 let paths = paths.to_vec();
151
152 let block = RcBlock::new(move || {
153 do_begin_drag(&paths);
154 });
155 // SAFETY: `_dispatch_main_q` is a valid process-global symbol provided by libdispatch.
156 // `RcBlock` ensures the closure outlives the async dispatch (prevent use-after-free).
157 unsafe {
158 dispatch_async(&_dispatch_main_q, &block);
159 }
160
161 true
162 }
163