//! macOS drag backend: initiates an NSDraggingSession via NSView. //! //! The drag must be deferred with `dispatch_async` because egui's render pass //! runs inside `drawRect:`, where AppKit cannot start a drag session. //! Dispatching to the main queue moves the call to the next runloop iteration. //! //! The `DRAG_ACTIVE` guard (in the parent module) stays set for the entire //! lifetime of the drag session, cleared only in the `draggingSession:endedAtPoint:` //! callback. This prevents re-entrant `beginDraggingSessionWithItems` calls //! (which return NULL and panic in objc2). use std::ffi::c_void; use std::path::PathBuf; use std::sync::atomic::Ordering; use block2::RcBlock; use objc2::rc::Retained; use objc2::runtime::{NSObject, NSObjectProtocol, ProtocolObject}; use objc2::{define_class, AnyThread, MainThreadMarker, MainThreadOnly}; use objc2_app_kit::{ NSApplication, NSDragOperation, NSDraggingContext, NSDraggingItem, NSDraggingSession, NSDraggingSource, NSEvent, NSEventModifierFlags, NSEventType, }; use objc2_foundation::{NSArray, NSPoint, NSRect, NSSize, NSURL}; use tracing::{debug, warn}; // SAFETY: `_dispatch_main_q` and `dispatch_async` are public symbols exported by // Apple's libdispatch. The declared signatures match the system headers; dispatch // is thread-safe and `RcBlock` keeps the closure alive across the async boundary. unsafe extern "C" { static _dispatch_main_q: c_void; fn dispatch_async(queue: *const c_void, block: &block2::Block); } // ---------- Drag source ---------- define_class!( #[unsafe(super(NSObject))] #[thread_kind = MainThreadOnly] #[name = "AFDragSource"] struct DragSource; unsafe impl NSObjectProtocol for DragSource {} unsafe impl NSDraggingSource for DragSource { #[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))] fn _source_op_mask( &self, _session: &NSDraggingSession, _context: NSDraggingContext, ) -> NSDragOperation { NSDragOperation::Copy } #[unsafe(method(draggingSession:endedAtPoint:operation:))] fn _session_ended( &self, _session: &NSDraggingSession, _screen_point: NSPoint, _operation: NSDragOperation, ) { debug!("Drag session ended"); super::DRAG_ACTIVE.store(false, Ordering::Release); } } ); impl DragSource { fn new(mtm: MainThreadMarker) -> Retained { // SAFETY: `alloc` + `init` is the standard NSObject construction pattern. // `MainThreadMarker` guarantees we're on the main thread, which `define_class!` // requires for `MainThreadOnly` types. `set_ivars(())` is infallible (no ivars). unsafe { objc2::msg_send![super(Self::alloc(mtm).set_ivars(())), init] } } } // ---------- Deferred drag execution ---------- /// Called on the main queue outside `drawRect:`. /// On failure, clears the DRAG_ACTIVE guard directly; on success, the /// `_session_ended` callback clears it when the drag session finishes. fn do_begin_drag(paths: &[PathBuf]) { if !try_begin_drag(paths) { super::DRAG_ACTIVE.store(false, Ordering::Release); } } fn try_begin_drag(paths: &[PathBuf]) -> bool { let Some(mtm) = MainThreadMarker::new() else { warn!("do_begin_drag: not on main thread"); return false; }; let app = NSApplication::sharedApplication(mtm); let Some(window) = app.keyWindow() else { warn!("do_begin_drag: no key window"); return false; }; let Some(view) = window.contentView() else { warn!("do_begin_drag: no content view"); return false; }; let location = window.mouseLocationOutsideOfEventStream(); let Some(event) = NSEvent::mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure( NSEventType::LeftMouseDragged, location, NSEventModifierFlags(0), 0.0, window.windowNumber(), None, 0, 1, 1.0, ) else { warn!("do_begin_drag: failed to create synthetic event"); return false; }; let items: Vec> = paths .iter() .filter_map(|path| { let url = NSURL::from_file_path(path)?; let writer: &ProtocolObject = ProtocolObject::from_ref(&*url); let item = NSDraggingItem::initWithPasteboardWriter(NSDraggingItem::alloc(), writer); item.setDraggingFrame(NSRect::new(location, NSSize::new(32.0, 32.0))); Some(item) }) .collect(); if items.is_empty() { warn!("do_begin_drag: no dragging items"); return false; } let items_ref: Vec<&NSDraggingItem> = items.iter().map(|i| &**i).collect(); let array = NSArray::from_slice(&items_ref); let source = DragSource::new(mtm); let source_proto: &ProtocolObject = ProtocolObject::from_ref(&*source); debug!(count = items.len(), "Starting NSDraggingSession"); let _session = view.beginDraggingSessionWithItems_event_source(&array, &event, source_proto); true } // ---------- Public entry point ---------- pub(super) fn begin_drag_session(paths: &[PathBuf]) -> bool { let paths = paths.to_vec(); let block = RcBlock::new(move || { do_begin_drag(&paths); }); // SAFETY: `_dispatch_main_q` is a valid process-global symbol provided by libdispatch. // `RcBlock` ensures the closure outlives the async dispatch (prevent use-after-free). unsafe { dispatch_async(&_dispatch_main_q, &block); } true }