use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::OnceLock;
use kui_core::Vec2;
use objc2::encode::Encode;
use objc2::rc::Retained;
use objc2::runtime::{AnyClass, AnyObject, Bool, Imp, Method, Sel};
use objc2::{ffi, msg_send, sel};
#[allow(deprecated)]
use objc2_app_kit::NSFilenamesPboardType;
use objc2_app_kit::{NSPasteboard, NSView};
use objc2_foundation::{NSArray, NSPoint, NSString};
use winit::window::Window;
use crate::Waker;
#[derive(Clone, Debug, PartialEq)]
pub enum DragMsg {
Over(Vec<String>, Vec2),
Drop(Vec<String>, Vec2),
Cancel,
}
const OP_NONE: usize = 0;
const OP_COPY: usize = 1;
#[derive(Clone, Copy, Debug, Default)]
struct Session {
accepts: bool,
settled: bool,
}
#[derive(Default)]
struct State {
sessions: HashMap<usize, Session>,
queue: Vec<(usize, DragMsg)>,
waker: Option<Waker>,
}
thread_local! {
static STATE: RefCell<State> = RefCell::new(State::default());
}
static INSTALLED: OnceLock<bool> = OnceLock::new();
fn view_of(window: &Window) -> Option<Retained<NSView>> {
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
let RawWindowHandle::AppKit(h) = window.window_handle().ok()?.as_raw() else {
return None;
};
unsafe { Retained::retain(h.ns_view.as_ptr().cast::<NSView>()) }
}
pub fn delegate_ptr(window: &Window) -> Option<usize> {
let view = view_of(window)?;
let ns_window = view.window()?;
let delegate = ns_window.delegate()?;
Some(Retained::as_ptr(&delegate) as *const AnyObject as usize)
}
pub fn set_waker(waker: Waker) {
STATE.with(|s| s.borrow_mut().waker = Some(waker));
}
pub fn attach(window: &Window) -> bool {
let Some(view) = view_of(window) else {
return false;
};
let Some(ns_window) = view.window() else {
return false;
};
let Some(delegate) = ns_window.delegate() else {
return false;
};
let ptr = Retained::as_ptr(&delegate) as *const AnyObject as usize;
let cls: &AnyClass = unsafe { msg_send![&*delegate, class] };
let installed = *INSTALLED.get_or_init(|| install(cls));
if installed {
STATE.with(|s| s.borrow_mut().sessions.insert(ptr, Session::default()));
}
installed
}
pub fn installed() -> bool {
INSTALLED.get().copied().unwrap_or(false)
}
pub fn detach(window: &Window) {
let Some(ptr) = delegate_ptr(window) else {
return;
};
STATE.with(|s| {
let mut s = s.borrow_mut();
s.sessions.remove(&ptr);
s.queue.retain(|(d, _)| *d != ptr);
});
}
pub fn stamp(window: &Window, accepts: bool) {
let Some(ptr) = delegate_ptr(window) else {
return;
};
STATE.with(|s| {
if let Some(session) = s.borrow_mut().sessions.get_mut(&ptr) {
session.accepts = accepts;
}
});
}
pub fn take_messages() -> Vec<(usize, DragMsg)> {
STATE.with(|s| std::mem::take(&mut s.borrow_mut().queue))
}
type EnteredFn = unsafe extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> usize;
type UpdatedFn = unsafe extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> usize;
type ExitedFn = unsafe extern "C-unwind" fn(&AnyObject, Sel, Option<&AnyObject>);
type PerformFn = unsafe extern "C-unwind" fn(&AnyObject, Sel, &AnyObject) -> Bool;
type EndedFn = unsafe extern "C-unwind" fn(&AnyObject, Sel, Option<&AnyObject>);
fn imp<F: Copy>(f: F) -> Imp {
const { assert!(std::mem::size_of::<F>() == std::mem::size_of::<Imp>()) };
unsafe { std::mem::transmute_copy(&f) }
}
fn defining_class(cls: &AnyClass, sel: Sel) -> Option<(&AnyClass, &Method)> {
let mut at = Some(cls);
while let Some(c) = at {
if let Some(m) = c
.instance_methods()
.iter()
.find(|m| m.name() == sel)
.copied()
{
return Some((c, m));
}
at = c.superclass();
}
None
}
fn install(cls: &AnyClass) -> bool {
let Some((owner, entered)) = defining_class(cls, sel!(draggingEntered:)) else {
return false;
};
let Some((_, exited)) = defining_class(cls, sel!(draggingExited:)) else {
return false;
};
let Some((_, perform)) = defining_class(cls, sel!(performDragOperation:)) else {
return false;
};
if defining_class(cls, sel!(draggingUpdated:)).is_some()
|| defining_class(cls, sel!(draggingEnded:)).is_some()
{
return false;
}
let owner = owner as *const AnyClass as *mut AnyClass;
let types_op = format!("{}@:@\0", usize::ENCODING);
let types_void = "v@:@\0";
let added = unsafe {
ffi::class_addMethod(
owner,
sel!(draggingUpdated:),
imp(updated_override as UpdatedFn),
types_op.as_ptr().cast(),
)
.as_bool()
&& ffi::class_addMethod(
owner,
sel!(draggingEnded:),
imp(ended_override as EndedFn),
types_void.as_ptr().cast(),
)
.as_bool()
};
if !added {
return false;
}
let imps: [(&Method, Imp); 3] = [
(entered, imp(entered_override as EnteredFn)),
(exited, imp(exited_override as ExitedFn)),
(perform, imp(perform_override as PerformFn)),
];
for (m, imp) in imps {
unsafe { m.set_implementation(imp) };
}
true
}
#[allow(deprecated)]
fn paths_of(sender: &AnyObject) -> Vec<String> {
let pb: Option<Retained<NSPasteboard>> = unsafe { msg_send![sender, draggingPasteboard] };
let Some(pb) = pb else {
return Vec::new();
};
let Some(list) = pb.propertyListForType(unsafe { NSFilenamesPboardType }) else {
return Vec::new();
};
let Ok(names) = list.downcast::<NSArray>() else {
return Vec::new();
};
names
.iter()
.filter_map(|s| s.downcast_ref::<NSString>().map(|s| s.to_string()))
.collect()
}
fn location_of(sender: &AnyObject) -> Vec2 {
let p: NSPoint = unsafe { msg_send![sender, draggingLocation] };
let window: Option<Retained<AnyObject>> =
unsafe { msg_send![sender, draggingDestinationWindow] };
let Some(window) = window else {
return Vec2::new(p.x as f32, p.y as f32);
};
let view: Option<Retained<NSView>> = unsafe { msg_send![&*window, contentView] };
let Some(view) = view else {
return Vec2::new(p.x as f32, p.y as f32);
};
let local = view.convertPoint_fromView(p, None);
Vec2::new(local.x as f32, local.y as f32)
}
fn push(this: &AnyObject, msg: DragMsg) {
let ptr = this as *const AnyObject as usize;
STATE.with(|s| {
let mut s = s.borrow_mut();
if !s.sessions.contains_key(&ptr) {
return;
}
s.queue.push((ptr, msg));
if let Some(w) = &s.waker {
w.wake();
}
});
}
fn session_of(this: &AnyObject) -> Option<Session> {
let ptr = this as *const AnyObject as usize;
STATE.with(|s| s.borrow().sessions.get(&ptr).copied())
}
fn update_session(this: &AnyObject, f: impl FnOnce(&mut Session)) {
let ptr = this as *const AnyObject as usize;
STATE.with(|s| {
if let Some(session) = s.borrow_mut().sessions.get_mut(&ptr) {
f(session);
}
});
}
unsafe extern "C-unwind" fn entered_override(
this: &AnyObject,
_sel: Sel,
sender: &AnyObject,
) -> usize {
if session_of(this).is_none() {
return OP_NONE;
}
update_session(this, |s| {
s.accepts = true;
s.settled = false;
});
let paths = paths_of(sender);
if paths.is_empty() {
return OP_NONE;
}
push(this, DragMsg::Over(paths, location_of(sender)));
OP_COPY
}
unsafe extern "C-unwind" fn updated_override(
this: &AnyObject,
_sel: Sel,
sender: &AnyObject,
) -> usize {
let Some(session) = session_of(this) else {
return OP_NONE;
};
let paths = paths_of(sender);
if paths.is_empty() {
return OP_NONE;
}
push(this, DragMsg::Over(paths, location_of(sender)));
if session.accepts { OP_COPY } else { OP_NONE }
}
unsafe extern "C-unwind" fn exited_override(
this: &AnyObject,
_sel: Sel,
_sender: Option<&AnyObject>,
) {
update_session(this, |s| s.settled = true);
push(this, DragMsg::Cancel);
}
unsafe extern "C-unwind" fn perform_override(
this: &AnyObject,
_sel: Sel,
sender: &AnyObject,
) -> Bool {
let Some(session) = session_of(this) else {
return Bool::NO;
};
update_session(this, |s| s.settled = true);
let paths = paths_of(sender);
if paths.is_empty() {
return Bool::NO;
}
push(this, DragMsg::Drop(paths, location_of(sender)));
Bool::new(session.accepts)
}
unsafe extern "C-unwind" fn ended_override(
this: &AnyObject,
_sel: Sel,
_sender: Option<&AnyObject>,
) {
let Some(session) = session_of(this) else {
return;
};
if !session.settled {
update_session(this, |s| s.settled = true);
push(this, DragMsg::Cancel);
}
}