use std::cell::RefCell;
use std::ffi::c_void;
use std::mem::ManuallyDrop;
use std::os::windows::ffi::OsStrExt;
use std::path::PathBuf;
use std::sync::Arc;
use raw_window_handle::RawWindowHandle;
use teksilo_canvas::Point;
use teksilo_core::AppEventPoster;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::window::TeksiloWindowId;
use teksilo_core::{DragImageData, DropOutcome, ExternalDropData, OutboundDragData};
use windows::Win32::Foundation::{
DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, DRAGDROP_S_USEDEFAULTCURSORS, DV_E_FORMATETC, E_NOTIMPL,
HGLOBAL, HWND, OLE_E_ADVISENOTSUPPORTED, POINT, POINTL, S_OK,
};
use windows::Win32::Graphics::Gdi::ScreenToClient;
use windows::Win32::System::Com::{
DATADIR_GET, DVASPECT_CONTENT, FORMATETC, IAdviseSink, IDataObject, IDataObject_Impl,
IEnumFORMATETC, IEnumSTATDATA, STGMEDIUM, STGMEDIUM_0, TYMED_HGLOBAL,
};
use windows::Win32::System::DataExchange::RegisterClipboardFormatW;
use windows::Win32::System::Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalUnlock};
use windows::Win32::System::Ole::{
CF_HDROP, CF_UNICODETEXT, DROPEFFECT, DROPEFFECT_COPY, DROPEFFECT_MOVE, DROPEFFECT_NONE,
DoDragDrop, IDropSource, IDropSource_Impl, IDropTarget, IDropTarget_Impl, OleInitialize,
RegisterDragDrop, ReleaseStgMedium, RevokeDragDrop,
};
use windows::Win32::System::SystemServices::{MK_LBUTTON, MK_RBUTTON, MODIFIERKEYS_FLAGS};
use windows::Win32::UI::HiDpi::GetDpiForWindow;
use windows::Win32::UI::Shell::{DROPFILES, DragQueryFileW, HDROP, SHCreateStdEnumFmtEtc};
use windows::core::{Ref, implement, w};
use super::{
ExternalDndBackend, ExternalDndEventPayload, ExternalDndGuard, ExternalDragEvent, NoopDndGuard,
OutboundOsDragRequest,
};
#[implement(IDropTarget)]
struct DropTarget {
hwnd: HWND,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
}
impl DropTarget {
fn post(&self, event: ExternalDragEvent) {
self.poster.post_external(Box::new(ExternalDndEventPayload {
window_id_owner: self.window_id,
event,
}));
}
fn logical_position(&self, pt: &POINTL) -> Point {
let mut p = POINT { x: pt.x, y: pt.y };
let _ = unsafe { ScreenToClient(self.hwnd, &mut p) };
let dpi = unsafe { GetDpiForWindow(self.hwnd) };
let scale = if dpi == 0 { 1.0 } else { dpi as f32 / 96.0 };
Point::new(p.x as f32 / scale, p.y as f32 / scale)
}
}
impl IDropTarget_Impl for DropTarget_Impl {
fn DragEnter(
&self,
pdataobj: Ref<'_, IDataObject>,
_grfkeystate: windows::Win32::System::SystemServices::MODIFIERKEYS_FLAGS,
pt: &POINTL,
pdweffect: *mut DROPEFFECT,
) -> windows::core::Result<()> {
let data = pdataobj.ok().map(read_payload).unwrap_or_default();
self.post(ExternalDragEvent::Entered {
data,
position: self.logical_position(pt),
});
unsafe { *pdweffect = DROPEFFECT_COPY };
Ok(())
}
fn DragOver(
&self,
_grfkeystate: windows::Win32::System::SystemServices::MODIFIERKEYS_FLAGS,
pt: &POINTL,
pdweffect: *mut DROPEFFECT,
) -> windows::core::Result<()> {
self.post(ExternalDragEvent::Moved {
position: self.logical_position(pt),
});
unsafe { *pdweffect = DROPEFFECT_COPY };
Ok(())
}
fn DragLeave(&self) -> windows::core::Result<()> {
self.post(ExternalDragEvent::Left);
Ok(())
}
fn Drop(
&self,
pdataobj: Ref<'_, IDataObject>,
_grfkeystate: windows::Win32::System::SystemServices::MODIFIERKEYS_FLAGS,
pt: &POINTL,
pdweffect: *mut DROPEFFECT,
) -> windows::core::Result<()> {
let data = pdataobj.ok().map(read_payload).unwrap_or_default();
let accepted = !data.is_empty();
self.post(ExternalDragEvent::Dropped {
data,
position: self.logical_position(pt),
});
unsafe {
*pdweffect = if accepted {
DROPEFFECT_COPY
} else {
DROPEFFECT_NONE
}
};
Ok(())
}
}
fn read_payload(data: &IDataObject) -> ExternalDropData {
let mut out = ExternalDropData::default();
if let Some(files) = read_hdrop(data) {
out.files = files;
}
if let Some(text) = read_unicode_text(data, CF_UNICODETEXT.0) {
if !text.is_empty() {
out.text = Some(text);
}
}
let inet_url = unsafe { RegisterClipboardFormatW(w!("UniformResourceLocatorW")) } as u16;
if inet_url != 0 {
if let Some(url) = read_unicode_text(data, inet_url) {
if !url.is_empty() {
out.uris.push(url);
}
}
}
out
}
fn with_hglobal<T>(
data: &IDataObject,
cf_format: u16,
f: impl FnOnce(*const c_void) -> T,
) -> Option<T> {
let fmt = FORMATETC {
cfFormat: cf_format,
ptd: std::ptr::null_mut(),
dwAspect: DVASPECT_CONTENT.0 as u32,
lindex: -1,
tymed: TYMED_HGLOBAL.0 as u32,
};
let mut medium: STGMEDIUM = unsafe { data.GetData(&fmt).ok()? };
let hglobal: HGLOBAL = unsafe { medium.u.hGlobal };
let ptr = unsafe { GlobalLock(hglobal) };
let result = if ptr.is_null() {
None
} else {
let r = f(ptr as *const c_void);
let _ = unsafe { GlobalUnlock(hglobal) };
Some(r)
};
unsafe { ReleaseStgMedium(&mut medium) };
result
}
fn read_hdrop(data: &IDataObject) -> Option<Vec<PathBuf>> {
with_hglobal(data, CF_HDROP.0, |ptr| {
let hdrop = HDROP(ptr as *mut c_void);
let count = unsafe { DragQueryFileW(hdrop, 0xFFFF_FFFF, None) };
let mut files = Vec::with_capacity(count as usize);
for i in 0..count {
let len = unsafe { DragQueryFileW(hdrop, i, None) } as usize;
if len == 0 {
continue;
}
let mut buf = vec![0u16; len + 1];
let written = unsafe { DragQueryFileW(hdrop, i, Some(&mut buf)) } as usize;
buf.truncate(written);
files.push(PathBuf::from(String::from_utf16_lossy(&buf)));
}
files
})
}
fn read_unicode_text(data: &IDataObject, cf_format: u16) -> Option<String> {
with_hglobal(data, cf_format, |ptr| {
let mut wide = ptr as *const u16;
let mut units = Vec::new();
unsafe {
while *wide != 0 {
units.push(*wide);
wide = wide.add(1);
}
}
String::from_utf16_lossy(&units)
})
}
pub struct WindowsDndGuard {
hwnd: HWND,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
pending: RefCell<Option<OutboundDragData>>,
_target: IDropTarget,
}
impl WindowsDndGuard {
fn post(&self, event: ExternalDragEvent) {
self.poster.post_external(Box::new(ExternalDndEventPayload {
window_id_owner: self.window_id,
event,
}));
}
}
impl ExternalDndGuard for WindowsDndGuard {
fn begin_drag(&self, data: &OutboundDragData, _image: Option<&DragImageData>) -> bool {
*self.pending.borrow_mut() = Some(data.clone());
self.poster.post_external(Box::new(OutboundOsDragRequest {
window_id: self.window_id,
}));
true
}
fn run_pending_outbound_drag(&self) {
let data = match self.pending.borrow_mut().take() {
Some(d) if !d.is_empty() => d,
_ => return,
};
let data_object: IDataObject = DataObject::from_data(&data).into();
let drop_source: IDropSource = DropSource.into();
let mut effect = DROPEFFECT_NONE;
let hr = unsafe { DoDragDrop(&data_object, &drop_source, DROPEFFECT_COPY, &mut effect) };
let outcome = if hr == DRAGDROP_S_DROP {
if effect == DROPEFFECT_MOVE {
DropOutcome::OsMove
} else if effect == DROPEFFECT_COPY {
DropOutcome::OsCopy
} else {
DropOutcome::Cancelled
}
} else {
DropOutcome::Cancelled
};
self.post(ExternalDragEvent::DragEnded { outcome });
}
}
impl Drop for WindowsDndGuard {
fn drop(&mut self) {
let _ = unsafe { RevokeDragDrop(self.hwnd) };
}
}
#[derive(Default)]
pub struct WindowsExternalDndBackend;
impl WindowsExternalDndBackend {
pub fn new() -> Self {
Self::default()
}
}
impl ExternalDndBackend for WindowsExternalDndBackend {
fn attach(
&mut self,
parent: ParentHandle,
window_id: TeksiloWindowId,
poster: Arc<dyn AppEventPoster>,
) -> Box<dyn ExternalDndGuard> {
let RawWindowHandle::Win32(handle) = parent.raw_window_handle() else {
return Box::new(NoopDndGuard);
};
let hwnd = HWND(handle.hwnd.get() as *mut c_void);
let _ = unsafe { OleInitialize(None) };
let _ = unsafe { RevokeDragDrop(hwnd) };
let target: IDropTarget = DropTarget {
hwnd,
window_id,
poster: poster.clone(),
}
.into();
if unsafe { RegisterDragDrop(hwnd, &target) }.is_err() {
return Box::new(NoopDndGuard);
}
Box::new(WindowsDndGuard {
hwnd,
window_id,
poster,
pending: RefCell::new(None),
_target: target,
})
}
}
struct FormatBlob {
cf: u16,
bytes: Vec<u8>,
}
#[implement(IDataObject)]
struct DataObject {
blobs: Vec<FormatBlob>,
formats: Vec<FORMATETC>,
}
impl DataObject {
fn from_data(data: &OutboundDragData) -> Self {
let mut blobs: Vec<FormatBlob> = Vec::new();
if !data.files.is_empty() {
blobs.push(FormatBlob {
cf: CF_HDROP.0,
bytes: build_hdrop(&data.files),
});
}
let has_text = data.text.as_deref().is_some_and(|t| !t.is_empty());
if let Some(text) = &data.text
&& !text.is_empty()
{
blobs.push(FormatBlob {
cf: CF_UNICODETEXT.0,
bytes: build_unicode_text(text),
});
}
if let Some(first_url) = data.uris.first() {
let inet_url =
unsafe { RegisterClipboardFormatW(w!("UniformResourceLocatorW")) } as u16;
if inet_url != 0 {
blobs.push(FormatBlob {
cf: inet_url,
bytes: build_unicode_text(first_url),
});
}
if !has_text {
blobs.push(FormatBlob {
cf: CF_UNICODETEXT.0,
bytes: build_unicode_text(first_url),
});
}
}
let formats = blobs.iter().map(|b| formatetc(b.cf)).collect();
Self { blobs, formats }
}
}
impl IDataObject_Impl for DataObject_Impl {
fn GetData(&self, pformatetcin: *const FORMATETC) -> windows_core::Result<STGMEDIUM> {
let fmt = unsafe { &*pformatetcin };
let wants_hglobal = fmt.tymed & TYMED_HGLOBAL.0 as u32 != 0;
if wants_hglobal
&& let Some(blob) = self.blobs.iter().find(|b| b.cf == fmt.cfFormat)
&& let Some(hglobal) = unsafe { alloc_hglobal(&blob.bytes) }
{
return Ok(STGMEDIUM {
tymed: TYMED_HGLOBAL.0 as u32,
u: STGMEDIUM_0 { hGlobal: hglobal },
pUnkForRelease: ManuallyDrop::new(None),
});
}
Err(windows_core::Error::from_hresult(DV_E_FORMATETC))
}
fn GetDataHere(
&self,
_pformatetc: *const FORMATETC,
_pmedium: *mut STGMEDIUM,
) -> windows_core::Result<()> {
Err(windows_core::Error::from_hresult(E_NOTIMPL))
}
fn QueryGetData(&self, pformatetc: *const FORMATETC) -> windows_core::HRESULT {
let fmt = unsafe { &*pformatetc };
let wants_hglobal = fmt.tymed & TYMED_HGLOBAL.0 as u32 != 0;
if wants_hglobal && self.blobs.iter().any(|b| b.cf == fmt.cfFormat) {
S_OK
} else {
DV_E_FORMATETC
}
}
fn GetCanonicalFormatEtc(
&self,
_pformatectin: *const FORMATETC,
_pformatetcout: *mut FORMATETC,
) -> windows_core::HRESULT {
E_NOTIMPL
}
fn SetData(
&self,
_pformatetc: *const FORMATETC,
_pmedium: *const STGMEDIUM,
_frelease: windows_core::BOOL,
) -> windows_core::Result<()> {
Err(windows_core::Error::from_hresult(E_NOTIMPL))
}
fn EnumFormatEtc(&self, dwdirection: u32) -> windows_core::Result<IEnumFORMATETC> {
if dwdirection == DATADIR_GET.0 as u32 {
unsafe { SHCreateStdEnumFmtEtc(&self.formats) }
} else {
Err(windows_core::Error::from_hresult(E_NOTIMPL))
}
}
fn DAdvise(
&self,
_pformatetc: *const FORMATETC,
_advf: u32,
_padvsink: Ref<IAdviseSink>,
) -> windows_core::Result<u32> {
Err(windows_core::Error::from_hresult(OLE_E_ADVISENOTSUPPORTED))
}
fn DUnadvise(&self, _dwconnection: u32) -> windows_core::Result<()> {
Err(windows_core::Error::from_hresult(OLE_E_ADVISENOTSUPPORTED))
}
fn EnumDAdvise(&self) -> windows_core::Result<IEnumSTATDATA> {
Err(windows_core::Error::from_hresult(OLE_E_ADVISENOTSUPPORTED))
}
}
#[implement(IDropSource)]
struct DropSource;
impl IDropSource_Impl for DropSource_Impl {
fn QueryContinueDrag(
&self,
fescapepressed: windows_core::BOOL,
grfkeystate: MODIFIERKEYS_FLAGS,
) -> windows_core::HRESULT {
let keys = grfkeystate.0;
if fescapepressed.as_bool() || (keys & MK_RBUTTON.0) != 0 {
DRAGDROP_S_CANCEL
} else if (keys & MK_LBUTTON.0) == 0 {
DRAGDROP_S_DROP
} else {
S_OK
}
}
fn GiveFeedback(&self, _dweffect: DROPEFFECT) -> windows_core::HRESULT {
DRAGDROP_S_USEDEFAULTCURSORS
}
}
fn formatetc(cf: u16) -> FORMATETC {
FORMATETC {
cfFormat: cf,
ptd: std::ptr::null_mut(),
dwAspect: DVASPECT_CONTENT.0,
lindex: -1,
tymed: TYMED_HGLOBAL.0 as u32,
}
}
unsafe fn alloc_hglobal(bytes: &[u8]) -> Option<HGLOBAL> {
let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, bytes.len()) }.ok()?;
let ptr = unsafe { GlobalLock(hglobal) };
if ptr.is_null() {
return None;
}
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
let _ = GlobalUnlock(hglobal);
}
Some(hglobal)
}
fn build_hdrop(files: &[PathBuf]) -> Vec<u8> {
let header = DROPFILES {
pFiles: std::mem::size_of::<DROPFILES>() as u32,
pt: POINT { x: 0, y: 0 },
fNC: windows_core::BOOL::from(false),
fWide: windows_core::BOOL::from(true),
};
let mut buf = Vec::new();
let header_bytes = unsafe {
std::slice::from_raw_parts(
(&header as *const DROPFILES) as *const u8,
std::mem::size_of::<DROPFILES>(),
)
};
buf.extend_from_slice(header_bytes);
for path in files {
for unit in path.as_os_str().encode_wide() {
buf.extend_from_slice(&unit.to_le_bytes());
}
buf.extend_from_slice(&0u16.to_le_bytes());
}
buf.extend_from_slice(&0u16.to_le_bytes());
buf
}
fn build_unicode_text(s: &str) -> Vec<u8> {
let mut buf = Vec::new();
for unit in s.encode_utf16() {
buf.extend_from_slice(&unit.to_le_bytes());
}
buf.extend_from_slice(&0u16.to_le_bytes());
buf
}