use std::ptr::NonNull;
use std::sync::Arc;
use teksilo_canvas::Point;
use teksilo_core::AppEventPoster;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::window::TeksiloWindowId;
use objc2::rc::Retained;
use objc2::runtime::{AnyObject, NSObjectProtocol, ProtocolObject};
use objc2::{AnyThread, ClassType, DefinedClass, MainThreadMarker, define_class, msg_send};
use objc2_app_kit::{
NSApplication, NSAutoresizingMaskOptions, NSDragOperation, NSDraggingContext,
NSDraggingDestination, NSDraggingInfo, NSDraggingItem, NSDraggingSession, NSDraggingSource,
NSImage, NSPasteboardTypeFileURL, NSPasteboardTypeString, NSPasteboardTypeURL,
NSPasteboardWriting, NSView, NSWorkspace,
};
use objc2_foundation::{NSArray, NSPoint, NSRect, NSSize, NSString, NSURL};
use raw_window_handle::RawWindowHandle;
use super::{
ExternalDndBackend, ExternalDndEventPayload, ExternalDndGuard, ExternalDragEvent, NoopDndGuard,
};
use teksilo_core::{DragImageData, DropOutcome, ExternalDropData, OutboundDragData};
struct DropViewIvars {
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
}
define_class!(
#[unsafe(super(NSView))]
#[name = "TeksiloExternalDropView"]
#[ivars = DropViewIvars]
struct DropView;
impl DropView {
#[unsafe(method(isFlipped))]
fn is_flipped(&self) -> bool {
true
}
#[unsafe(method(hitTest:))]
fn hit_test(&self, _point: NSPoint) -> *mut NSView {
std::ptr::null_mut()
}
}
unsafe impl NSObjectProtocol for DropView {}
unsafe impl NSDraggingDestination for DropView {
#[unsafe(method(draggingEntered:))]
fn dragging_entered(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> NSDragOperation {
let position = self.local_position(sender);
let data = read_pasteboard(sender);
self.post(ExternalDragEvent::Entered { data, position });
NSDragOperation::Copy
}
#[unsafe(method(draggingUpdated:))]
fn dragging_updated(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> NSDragOperation {
let position = self.local_position(sender);
self.post(ExternalDragEvent::Moved { position });
NSDragOperation::Copy
}
#[unsafe(method(draggingExited:))]
fn dragging_exited(&self, _sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) {
self.post(ExternalDragEvent::Left);
}
#[unsafe(method(performDragOperation:))]
fn perform_drag_operation(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
let position = self.local_position(sender);
let data = read_pasteboard(sender);
let accepted = !data.is_empty();
self.post(ExternalDragEvent::Dropped { data, position });
accepted
}
}
unsafe impl NSDraggingSource for DropView {
#[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))]
fn dragging_source_operation_mask(
&self,
_session: &NSDraggingSession,
_context: NSDraggingContext,
) -> NSDragOperation {
NSDragOperation::Copy
}
#[unsafe(method(draggingSession:endedAtPoint:operation:))]
fn dragging_source_ended(
&self,
_session: &NSDraggingSession,
_screen_point: NSPoint,
operation: NSDragOperation,
) {
let outcome = if operation.is_empty() {
DropOutcome::Cancelled
} else if operation.contains(NSDragOperation::Move) {
DropOutcome::OsMove
} else {
DropOutcome::OsCopy
};
self.post(ExternalDragEvent::DragEnded { outcome });
}
}
);
impl DropView {
fn new(
mtm: MainThreadMarker,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
frame: objc2_foundation::NSRect,
) -> Retained<Self> {
let this = mtm
.alloc::<Self>()
.set_ivars(DropViewIvars { window_id, poster });
unsafe { msg_send![super(this), initWithFrame: frame] }
}
fn local_position(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> Point {
let window_loc = sender.draggingLocation();
let local = self.convertPoint_fromView(window_loc, None);
Point::new(local.x as f32, local.y as f32)
}
fn post(&self, event: ExternalDragEvent) {
let ivars = self.ivars();
ivars
.poster
.post_external(Box::new(ExternalDndEventPayload {
window_id_owner: ivars.window_id,
event,
}));
}
fn begin_drag(&self, data: &OutboundDragData, _image: Option<&DragImageData>) -> bool {
let Some(mtm) = MainThreadMarker::new() else {
return false;
};
let app = NSApplication::sharedApplication(mtm);
let Some(event) = app.currentEvent() else {
return false;
};
let origin = self.convertPoint_fromView(event.locationInWindow(), None);
let frame = NSRect::new(origin, NSSize::new(32.0, 32.0));
let workspace = NSWorkspace::sharedWorkspace();
let mut items: Vec<Retained<NSDraggingItem>> = Vec::new();
for path in &data.files {
let s = NSString::from_str(&path.to_string_lossy());
let url = NSURL::fileURLWithPath(&s);
let writer: &ProtocolObject<dyn NSPasteboardWriting> = ProtocolObject::from_ref(&*url);
let item = NSDraggingItem::initWithPasteboardWriter(NSDraggingItem::alloc(), writer);
let icon = workspace.iconForFile(&s);
unsafe {
item.setDraggingFrame_contents(frame, Some(AsRef::<AnyObject>::as_ref(&*icon)));
}
items.push(item);
}
for uri in &data.uris {
let s = NSString::from_str(uri);
let Some(url) = NSURL::URLWithString(&s) else {
continue;
};
let writer: &ProtocolObject<dyn NSPasteboardWriting> = ProtocolObject::from_ref(&*url);
let item = NSDraggingItem::initWithPasteboardWriter(NSDraggingItem::alloc(), writer);
let img = placeholder_image(mtm);
unsafe {
item.setDraggingFrame_contents(frame, Some(AsRef::<AnyObject>::as_ref(&*img)));
}
items.push(item);
}
if let Some(text) = &data.text {
let s = NSString::from_str(text);
let writer: &ProtocolObject<dyn NSPasteboardWriting> = ProtocolObject::from_ref(&*s);
let item = NSDraggingItem::initWithPasteboardWriter(NSDraggingItem::alloc(), writer);
let img = placeholder_image(mtm);
unsafe {
item.setDraggingFrame_contents(frame, Some(AsRef::<AnyObject>::as_ref(&*img)));
}
items.push(item);
}
if items.is_empty() {
return false;
}
let array = NSArray::from_retained_slice(&items);
let source: &ProtocolObject<dyn NSDraggingSource> = ProtocolObject::from_ref(self);
self.beginDraggingSessionWithItems_event_source(&array, &event, source);
true
}
}
fn placeholder_image(_mtm: MainThreadMarker) -> Retained<NSImage> {
NSImage::initWithSize(NSImage::alloc(), NSSize::new(16.0, 16.0))
}
fn read_pasteboard(sender: &ProtocolObject<dyn NSDraggingInfo>) -> ExternalDropData {
let pb = sender.draggingPasteboard();
let mut data = ExternalDropData::default();
let classes = NSArray::from_slice(&[NSURL::class()]);
if let Some(objects) = unsafe { pb.readObjectsForClasses_options(&classes, None) } {
for i in 0..objects.count() {
let obj: Retained<AnyObject> = objects.objectAtIndex(i);
if let Ok(url) = obj.downcast::<NSURL>() {
if url.isFileURL() {
if let Some(path) = url.path() {
data.files.push(std::path::PathBuf::from(path.to_string()));
}
} else if let Some(abs) = url.absoluteString() {
data.uris.push(abs.to_string());
}
}
}
}
if let Some(text) = pb.stringForType(unsafe { NSPasteboardTypeString }) {
let s = text.to_string();
if !s.is_empty() {
data.text = Some(s);
}
}
data
}
pub struct MacOsDndGuard {
overlay: Retained<DropView>,
}
impl ExternalDndGuard for MacOsDndGuard {
fn begin_drag(&self, data: &OutboundDragData, image: Option<&DragImageData>) -> bool {
self.overlay.begin_drag(data, image)
}
}
impl Drop for MacOsDndGuard {
fn drop(&mut self) {
self.overlay.removeFromSuperview();
}
}
#[derive(Default)]
pub struct MacOsExternalDndBackend;
impl MacOsExternalDndBackend {
pub fn new() -> Self {
Self
}
}
impl ExternalDndBackend for MacOsExternalDndBackend {
fn attach(
&mut self,
parent: ParentHandle,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
) -> Box<dyn ExternalDndGuard> {
let Some(mtm) = MainThreadMarker::new() else {
return Box::new(NoopDndGuard);
};
let RawWindowHandle::AppKit(handle) = parent.raw_window_handle() else {
return Box::new(NoopDndGuard);
};
let ns_view_ptr: NonNull<NSView> = handle.ns_view.cast();
let content_view: Retained<NSView> = unsafe { Retained::retain(ns_view_ptr.as_ptr()) }
.expect("AppKit window handle has a live NSView");
let bounds = content_view.bounds();
let overlay = DropView::new(mtm, window_id, poster, bounds);
overlay.setAutoresizingMask(
NSAutoresizingMaskOptions::ViewWidthSizable
| NSAutoresizingMaskOptions::ViewHeightSizable,
);
let types = NSArray::from_slice(&[
unsafe { NSPasteboardTypeFileURL },
unsafe { NSPasteboardTypeURL },
unsafe { NSPasteboardTypeString },
]);
overlay.registerForDraggedTypes(&types);
content_view.addSubview(&overlay);
Box::new(MacOsDndGuard { overlay })
}
}