Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ windows = { version = "=0.61.3", features = [
"Win32_Graphics_Gdi",
"Win32_Graphics_OpenGL",
"Win32_System_Com_StructuredStorage",
"Win32_System_DataExchange",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_System_SystemServices",
"Win32_UI_WindowsAndMessaging",
Expand Down
2 changes: 2 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ pub enum DropEffect {
pub enum DropData {
None,
Files(Vec<PathBuf>),
/// A URL being dragged, e.g. from a web browser.
Url(String),
}

/// Return value for [WindowHandler::on_event](`crate::WindowHandler::on_event()`),
Expand Down
45 changes: 29 additions & 16 deletions src/macos/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ use objc2::runtime::{
};
use objc2::{msg_send, sel, AllocAnyThread, ClassType};
use objc2_app_kit::{
NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSWindow, NSWindowDidBecomeKeyNotification,
NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSPasteboardTypeURL,
NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, NSWindowDidBecomeKeyNotification,
NSWindowDidResignKeyNotification,
};
use objc2_core_foundation::CFUUID;
use objc2_foundation::{
NSArray, NSNotification, NSNotificationCenter, NSPoint, NSRect, NSSize, NSString,
};
use std::ffi::{c_void, CStr, CString};
use std::path::PathBuf;

use super::keyboard::make_modifiers;
use super::window::WindowState;
Expand Down Expand Up @@ -116,9 +117,13 @@ pub(super) fn create_view(window_options: &WindowOpenOptions) -> Retained<NSView
);
}

// SAFETY: This static is a read-only constant
// SAFETY: These statics are read-only constants
let ns_filenames_pboard_type = unsafe { NSFilenamesPboardType };
view.registerForDraggedTypes(&NSArray::from_slice(&[ns_filenames_pboard_type]));
let ns_url_pboard_type = unsafe { NSPasteboardTypeURL };
view.registerForDraggedTypes(&NSArray::from_slice(&[
ns_filenames_pboard_type,
ns_url_pboard_type,
]));

view
}
Expand Down Expand Up @@ -485,21 +490,29 @@ fn get_drop_data(sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) -> DropDat
};

let pasteboard = sender.draggingPasteboard();
let Some(file_list) = pasteboard.propertyListForType(unsafe { NSFilenamesPboardType }) else {
return DropData::None;
};

let Ok(file_list) = file_list.downcast::<NSArray>() else {
return DropData::None;
};
if let Some(file_list) = pasteboard.propertyListForType(unsafe { NSFilenamesPboardType }) {
if let Ok(file_list) = file_list.downcast::<NSArray>() {
let files: Vec<PathBuf> = file_list
.into_iter()
.filter_map(|s| s.downcast::<NSString>().ok())
.map(|s| PathBuf::from(s.to_string()))
.collect();

if !files.is_empty() {
return DropData::Files(files);
}
}
}

let files = file_list
.into_iter()
.filter_map(|s| s.downcast::<NSString>().ok())
.map(|s| s.to_string().into())
.collect();
if let Some(url) = pasteboard.stringForType(unsafe { NSPasteboardTypeURL }) {
let url = url.to_string();
if !url.is_empty() {
return DropData::Url(url);
}
}

DropData::Files(files)
DropData::None
}

fn on_event(window_state: &WindowState, event: MouseEvent) -> NSDragOperation {
Expand Down
186 changes: 186 additions & 0 deletions src/win/data_object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use std::ffi::OsStr;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::PathBuf;
use std::ptr::null_mut;
use std::sync::OnceLock;
use windows::Win32::Foundation::S_OK;
use windows::Win32::System::Com::{IDataObject, DATADIR_GET, FORMATETC, STGMEDIUM, TYMED_HGLOBAL};
use windows::Win32::System::DataExchange::{RegisterClipboardFormatA, RegisterClipboardFormatW};
use windows::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock};
use windows::Win32::System::Ole::{ReleaseStgMedium, CF_HDROP};
use windows_core::{PCSTR, PCWSTR};
use windows_sys::Win32::UI::Shell::DragQueryFileW;

use crate::DropData;

pub unsafe fn parse_data_object(data_object: &IDataObject) -> DropData {
let formats = enumerate_available_formats(data_object);

if let Some(format) = formats.iter().find(|f| f.cfFormat == CF_HDROP.0) {
if let Some(paths) =
with_format(data_object, *format, |medium| parse_files_from_medium(medium))
{
return DropData::Files(paths);
}
}

if let Some(format) = formats.iter().find(|f| f.cfFormat == uniform_resource_locator_w_format())
{
if let Some(url) = with_format(data_object, *format, |medium| {
read_utf16_null_terminated_from_medium(medium)
}) {
return DropData::Url(url);
}
}

if let Some(format) = formats.iter().find(|f| f.cfFormat == uniform_resource_locator_format()) {
if let Some(url) =
with_format(data_object, *format, |medium| read_ansi_c_string_from_medium(medium))
{
return DropData::Url(url);
}
}

DropData::None
}

unsafe fn enumerate_available_formats(data_object: &IDataObject) -> Vec<FORMATETC> {
let Ok(enumerator) = data_object.EnumFormatEtc(DATADIR_GET.0 as u32) else {
return Vec::new();
};

let mut formats = Vec::new();

loop {
let mut batch = [FORMATETC::default()];
let mut fetched = 0u32;
let hr = enumerator.Next(&mut batch, Some(&mut fetched));
if hr != S_OK || fetched == 0 {
break;
}

let format = batch[0];
if data_object.QueryGetData(&format as *const FORMATETC) == S_OK {
formats.push(format);
}
}

formats
}

fn uniform_resource_locator_w_format() -> u16 {
static ID: OnceLock<u16> = OnceLock::new();
*ID.get_or_init(|| {
let name: Vec<u16> =
OsStr::new("UniformResourceLocatorW").encode_wide().chain(std::iter::once(0)).collect();
unsafe { RegisterClipboardFormatW(PCWSTR(name.as_ptr())) as u16 }
})
}

fn uniform_resource_locator_format() -> u16 {
static ID: OnceLock<u16> = OnceLock::new();
*ID.get_or_init(|| unsafe {
RegisterClipboardFormatA(PCSTR(c"UniformResourceLocator".as_ptr().cast())) as u16
})
}

unsafe fn with_format<T>(
data_object: &IDataObject, format: FORMATETC, f: impl FnOnce(&STGMEDIUM) -> Option<T>,
) -> Option<T> {
let mut medium = data_object.GetData(&format).ok()?;
let result = f(&medium);
ReleaseStgMedium(&mut medium as *mut STGMEDIUM);
result
}

unsafe fn parse_files_from_medium(medium: &STGMEDIUM) -> Option<Vec<PathBuf>> {
if medium.tymed != TYMED_HGLOBAL.0 as u32 {
return None;
}

let hdrop = medium.u.hGlobal.0;
let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, null_mut(), 0);
if item_count == 0 {
return None;
}

let mut paths = Vec::with_capacity(item_count as usize);

for i in 0..item_count {
let characters = DragQueryFileW(hdrop, i, null_mut(), 0);
let buffer_size = characters as usize + 1;
let mut buffer = vec![0u16; buffer_size];

DragQueryFileW(hdrop, i, buffer.as_mut_ptr().cast(), buffer_size as u32);

paths.push(std::ffi::OsString::from_wide(&buffer[..characters as usize]).into());
}

Some(paths)
}

unsafe fn read_utf16_null_terminated_from_medium(medium: &STGMEDIUM) -> Option<String> {
if medium.tymed != TYMED_HGLOBAL.0 as u32 {
return None;
}

let hglobal = medium.u.hGlobal;
if hglobal.0.is_null() {
return None;
}

let size = GlobalSize(hglobal);
if size < 2 {
return None;
}

let p = GlobalLock(hglobal);
if p.is_null() {
return None;
}

let wide = std::slice::from_raw_parts(p as *const u16, size / 2);
let end = wide.iter().position(|&c| c == 0).unwrap_or(wide.len());
let s: String = std::char::decode_utf16(wide[..end].iter().copied())
.map(|r| r.unwrap_or(std::char::REPLACEMENT_CHARACTER))
.collect();
let _ = GlobalUnlock(hglobal);

if s.is_empty() {
None
} else {
Some(s)
}
}

unsafe fn read_ansi_c_string_from_medium(medium: &STGMEDIUM) -> Option<String> {
if medium.tymed != TYMED_HGLOBAL.0 as u32 {
return None;
}

let hglobal = medium.u.hGlobal;
if hglobal.0.is_null() {
return None;
}

let size = GlobalSize(hglobal);
if size == 0 {
return None;
}

let p = GlobalLock(hglobal);
if p.is_null() {
return None;
}

let bytes = std::slice::from_raw_parts(p as *const u8, size);
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
let s = String::from_utf8_lossy(&bytes[..end]).into_owned();
let _ = GlobalUnlock(hglobal);

if s.is_empty() {
None
} else {
Some(s)
}
}
48 changes: 5 additions & 43 deletions src/win/drop_target.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
use std::cell::{Cell, RefCell};
use std::ffi::OsString;
use std::os::windows::prelude::OsStringExt;
use std::ptr::null_mut;
use std::rc::Weak;
use windows::core::implement;
use windows::Win32::Foundation::{E_UNEXPECTED, POINTL};
use windows::Win32::System::Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL};
use windows::Win32::System::Com::IDataObject;
use windows::Win32::System::Ole::*;
use windows::Win32::System::SystemServices::MODIFIERKEYS_FLAGS;
use windows_core::Ref;
use windows_sys::Win32::{
Foundation::POINT, Graphics::Gdi::ScreenToClient, UI::Shell::DragQueryFileW,
};
use windows_sys::Win32::{Foundation::POINT, Graphics::Gdi::ScreenToClient};

use crate::{DropData, DropEffect, Event, EventStatus, MouseEvent, PhyPoint, Point};

use super::data_object;
use super::WindowState;

#[implement(IDropTarget)]
Expand Down Expand Up @@ -72,42 +68,8 @@ impl DropTarget {
}

fn parse_drop_data(&self, data_object: &IDataObject) {
let format = FORMATETC {
cfFormat: CF_HDROP.0,
ptd: null_mut(),
dwAspect: DVASPECT_CONTENT.0,
lindex: -1,
tymed: TYMED_HGLOBAL.0 as u32,
};

unsafe {
let Ok(medium) = data_object.GetData(&format) else {
self.drop_data.replace(DropData::None);
return;
};

let hdrop = medium.u.hGlobal.0;

let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, null_mut(), 0);
if item_count == 0 {
self.drop_data.replace(DropData::None);
return;
}

let mut paths = Vec::with_capacity(item_count as usize);

for i in 0..item_count {
let characters = DragQueryFileW(hdrop, i, null_mut(), 0);
let buffer_size = characters as usize + 1;
let mut buffer = vec![0u16; buffer_size];

DragQueryFileW(hdrop, i, buffer.as_mut_ptr().cast(), buffer_size as u32);

paths.push(OsString::from_wide(&buffer[..characters as usize]).into())
}

self.drop_data.replace(DropData::Files(paths));
}
let drop_data = unsafe { data_object::parse_data_object(data_object) };
self.drop_data.replace(drop_data);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/win/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod cursor;
mod data_object;
mod drop_target;
mod hook;
mod keyboard;
Expand Down