diff --git a/Cargo.toml b/Cargo.toml index 09ab59c92..4a82ad8ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"] } serde = { version = "1", features = ["serde_derive"] } smol_str = "0.3" tracing = { version = "0.1.40", default-features = false } +url = "2" # Dev dependencies. image = { version = "0.25.0", default-features = false } diff --git a/typos.toml b/typos.toml index c454f23b9..20ebeff8c 100644 --- a/typos.toml +++ b/typos.toml @@ -5,6 +5,7 @@ TME_LEAVE = "TME_LEAVE" # From windows_sys::Win32::UI::Input::Keyboa XF86_Calculater = "XF86_Calculater" # From xkbcommon_dl::keysyms::XF86_Calculater ptd = "ptd" # From windows_sys::Win32::System::Com::FORMATETC { ptd, ..} requestor = "requestor" # From x11_dl::xlib::XSelectionEvent { requestor ..} +unknwn = "unknwn" # Windows SDK header filename `unknwn.h` [files] extend-exclude = ["*.drawio"] diff --git a/winit-appkit/Cargo.toml b/winit-appkit/Cargo.toml index 109451062..54b366a29 100644 --- a/winit-appkit/Cargo.toml +++ b/winit-appkit/Cargo.toml @@ -36,6 +36,8 @@ objc2-app-kit = { workspace = true, features = [ "NSControl", "NSCursor", "NSDragging", + "NSDraggingItem", + "NSDraggingSession", "NSEvent", "NSGraphics", "NSGraphicsContext", @@ -46,6 +48,7 @@ objc2-app-kit = { workspace = true, features = [ "NSOpenGLView", "NSPanel", "NSPasteboard", + "NSPasteboardItem", "NSResponder", "NSRunningApplication", "NSScreen", diff --git a/winit-appkit/src/app_state.rs b/winit-appkit/src/app_state.rs index 5b057b79b..7c2d13790 100644 --- a/winit-appkit/src/app_state.rs +++ b/winit-appkit/src/app_state.rs @@ -1,4 +1,5 @@ use std::cell::{Cell, OnceCell, RefCell}; +use std::collections::HashMap; use std::mem; use std::rc::Rc; use std::sync::Arc; @@ -6,22 +7,30 @@ use std::time::Instant; use dispatch2::MainThreadBound; use objc2::MainThreadMarker; -use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication}; +use objc2::rc::{Retained, Weak}; +use objc2_app_kit::{ + NSApplication, NSApplicationActivationPolicy, NSDragOperation, NSRunningApplication, +}; use objc2_foundation::NSNotification; use winit_common::core_foundation::{EventLoopProxy, MainRunLoop}; use winit_common::event_handler::EventHandler; use winit_core::application::ApplicationHandler; +use winit_core::data_transfer::DataTransferId; use winit_core::event::{StartCause, WindowEvent}; -use winit_core::event_loop::ControlFlow; +use winit_core::event_loop::{ControlFlow, DndAction}; use winit_core::window::WindowId; use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately}; use super::menu; use super::observer::EventLoopWaker; +use crate::dnd::Pasteboards; +use crate::window_delegate::WindowDelegate; #[derive(Debug)] pub(super) struct AppState { mtm: MainThreadMarker, + drag_state: RefCell>, + pasteboards: Pasteboards, activation_policy: Option, default_menu: bool, activate_ignoring_other_apps: bool, @@ -43,10 +52,17 @@ pub(super) struct AppState { start_time: Cell>, wait_timeout: Cell>, pending_redraw: RefCell>, + windows: RefCell>>>, // NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and // as such should be careful to not add fields that, in turn, strongly reference those. } +#[derive(Debug)] +pub(crate) struct DragState { + pub id: DataTransferId, + pub valid_actions: Vec, +} + // SAFETY: Creating `MainThreadBound` in a `const` context, where there is no concept of the // main thread. static GLOBAL: MainThreadBound>> = @@ -65,6 +81,8 @@ impl AppState { let this = Rc::new(Self { mtm, + pasteboards: Default::default(), + drag_state: Default::default(), activation_policy, default_menu, activate_ignoring_other_apps, @@ -82,6 +100,7 @@ impl AppState { waker: RefCell::new(EventLoopWaker::new()), start_time: Cell::new(None), wait_timeout: Cell::new(None), + windows: Default::default(), pending_redraw: RefCell::new(vec![]), }); @@ -96,6 +115,20 @@ impl AppState { .clone() } + pub fn with_window_delegate_on_main(&self, id: WindowId, func: F) -> Option + where + F: FnOnce(Retained) -> R + Send, + R: Send, + { + self.windows.borrow_mut().get(&id)?.get_on_main(move |delegate| delegate.load().map(func)) + } + + pub fn register_window(&self, window: &Retained, mtm: MainThreadMarker) { + let id = window.id(); + let window_downgraded = Weak::from_retained(window); + self.windows.borrow_mut().insert(id, MainThreadBound::new(window_downgraded, mtm)); + } + // NOTE: This notification will, globally, only be emitted once, // no matter how many `EventLoop`s the user creates. pub fn did_finish_launching(self: &Rc, _notification: &NSNotification) { @@ -371,6 +404,23 @@ impl AppState { }; self.waker.borrow_mut().start_at(min_timeout(wait_timeout, app_timeout)); } + + pub fn pasteboards(&self) -> &Pasteboards { + &self.pasteboards + } + + pub fn drag_state(&self) -> &RefCell> { + &self.drag_state + } + + pub(crate) fn proposed_drag_action( + &self, + source_operations: NSDragOperation, + ) -> Option { + self.drag_state().borrow().as_ref().and_then(|drag_state| { + crate::dnd::preferred_drag_operation(source_operations, &drag_state.valid_actions) + }) + } } /// Returns the minimum `Option`, taking into account that `None` diff --git a/winit-appkit/src/cursor.rs b/winit-appkit/src/cursor.rs index e6e3337ff..be47e4365 100644 --- a/winit-appkit/src/cursor.rs +++ b/winit-appkit/src/cursor.rs @@ -14,6 +14,7 @@ use objc2_foundation::{ }; use winit_core::cursor::{CursorIcon, CursorImage, CustomCursorProvider, CustomCursorSource}; use winit_core::error::{NotSupportedError, RequestError}; +use winit_core::icon::{Icon, RgbaIcon}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct CustomCursor(pub(crate) Retained); @@ -42,6 +43,40 @@ impl CustomCursor { } } +pub(crate) fn image_from_icon(icon: &Icon) -> Result, RequestError> { + let rgba_icon = icon + .cast_ref::() + .ok_or(NotSupportedError::new("Only RGBA icons can be converted to `NSImage`"))?; + + let width = rgba_icon.width(); + let height = rgba_icon.height(); + + let bitmap = unsafe { + NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel( + NSBitmapImageRep::alloc(), + std::ptr::null_mut::<*mut c_uchar>(), + width as isize, + height as isize, + 8, + 4, + true, + false, + NSDeviceRGBColorSpace, + width as isize * 4, + 32, + ) + }.ok_or_else(|| os_error!("Initializing the `NSBitmapImageRep` failed"))?; + + let bitmap_data = + unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), rgba_icon.buffer().len()) }; + bitmap_data.copy_from_slice(rgba_icon.buffer()); + + let image = NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into())); + image.addRepresentation(&bitmap); + + Ok(image) +} + pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Result, RequestError> { let width = cursor.width(); let height = cursor.height(); diff --git a/winit-appkit/src/dnd.rs b/winit-appkit/src/dnd.rs new file mode 100644 index 000000000..1a4c86f41 --- /dev/null +++ b/winit-appkit/src/dnd.rs @@ -0,0 +1,499 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::io; +use std::ops::{BitOr, ControlFlow}; +use std::sync::{Arc, OnceLock}; + +use dispatch2::MainThreadBound; +use objc2::rc::{Retained, Weak}; +use objc2::runtime::AnyObject; +use objc2::{AnyThread, DefinedClass as _, MainThreadMarker, Message, define_class, msg_send}; +use objc2_app_kit::{ + NSDragOperation, NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, + NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, + NSPasteboardWriting, NSPasteboardWritingOptions, +}; +use objc2_foundation::{NSArray, NSData, NSObject, NSObjectProtocol, NSString}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, +}; +use winit_core::event_loop::DndAction; +use winit_core::window::WindowId; + +/// A thin wrapper around [`NSPasteboardType`], implementing [`TransferType`]. +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct PasteboardType { + hint: Option, + // We need to convert `NSString` to `str` since `NSString` isn't `Send`/`Sync` + inner: Arc, +} + +impl PasteboardType { + fn from_hint(hint: TypeHint) -> Option { + let hint_to_pasteboard_type = unsafe { + [ + (TypeHint::UriList, NSPasteboardTypeFileURL), + (TypeHint::Plaintext, NSPasteboardTypeString), + (TypeHint::Html, NSPasteboardTypeHTML), + (TypeHint::Image { extension_hint: Some("png") }, NSPasteboardTypePNG), + (TypeHint::Image { extension_hint: Some("tiff") }, NSPasteboardTypeTIFF), + (TypeHint::Audio { extension_hint: None }, NSPasteboardTypeSound), + ] + }; + + hint_to_pasteboard_type.into_iter().find_map(|(haystack, inner)| { + (haystack.matches(&hint)) + .then(|| Self { hint: Some(hint), inner: inner.to_string().into() }) + }) + } +} + +impl From> for PasteboardType { + fn from(value: Retained) -> Self { + let pasteboard_type_to_hint = unsafe { + [ + // Just in case the source application uses the deprecated method, we handle it + // here + #[expect(deprecated)] + (objc2_app_kit::NSFilenamesPboardType, TypeHint::UriList), + (NSPasteboardTypeFileURL, TypeHint::UriList), + (NSPasteboardTypeString, TypeHint::Plaintext), + (NSPasteboardTypeHTML, TypeHint::Html), + (NSPasteboardTypePNG, TypeHint::Image { extension_hint: Some("png") }), + (NSPasteboardTypeTIFF, TypeHint::Image { extension_hint: Some("tiff") }), + (NSPasteboardTypeSound, TypeHint::Audio { extension_hint: None }), + ] + }; + + let hint = pasteboard_type_to_hint + .iter() + .find_map(|(pb_type, hint)| (**pb_type == *value).then_some(hint)); + + Self { hint: hint.copied(), inner: value.to_string().into() } + } +} + +impl TransferType for PasteboardType { + fn hint(&self) -> Option { + self.hint + } + + fn matches(&self, other: &dyn TransferType) -> bool { + if let Some(other_pb_type) = other.cast_ref::() { + *self == *other_pb_type + } else { + // If either hint is `None`, return false + self.hint().is_some_and(|hint| other.hint() == Some(hint)) + } + } +} + +/// A thin wrapper around [`NSPasteboard`], implementing [`DataTransfer`]. +#[derive(Debug)] +pub struct Pasteboard { + transfer_id: DataTransferId, + ns_pasteboard: MainThreadBound>, + types: OnceLock>, +} + +impl Clone for Pasteboard { + fn clone(&self) -> Self { + let inner = self.ns_pasteboard.get_on_main(|inner| { + MainThreadBound::new(inner.clone(), MainThreadMarker::new().unwrap()) + }); + + Self { transfer_id: self.transfer_id, ns_pasteboard: inner, types: self.types.clone() } + } +} + +impl Pasteboard { + fn new( + transfer_id: DataTransferId, + ns_pasteboard: MainThreadBound>, + ) -> Self { + Self { transfer_id, ns_pasteboard, types: Default::default() } + } + + /// Get the array of [`PasteboardType`]s advertized by this [`Pasteboard`]. + pub fn types(&self) -> &[PasteboardType] { + self.types.get_or_init(|| { + self.ns_pasteboard.get_on_main(|pb| { + pb.types() + .map(|types| types.into_iter().map(PasteboardType::from).collect::>()) + .unwrap_or_default() + .into() + }) + }) + } + + /// Get the `DataTransferId` of this pasteboard. + pub fn id(&self) -> DataTransferId { + self.transfer_id + } + + /// Get a typed reader for this pasteboard. This is only necessary in the cross-platform case, + /// as a user downcasting to the platform-specific type can just access the `NSPasteboard` + /// directly. + pub(crate) fn with_type(&self, type_: PasteboardTypeSpec) -> PasteboardValue { + PasteboardValue { type_, pasteboard: self.clone() } + } +} + +impl DataTransfer for Pasteboard { + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, + ) { + let _ = self.types().iter().map(|mime| mime as &dyn TransferType).try_for_each(func); + } +} + +#[derive(Debug, Clone)] +pub(crate) enum PasteboardTypeSpec { + PasteboardType(PasteboardType), + TypeHint(TypeHint), +} + +impl PasteboardTypeSpec { + pub(crate) fn from_dyn(type_: &dyn TransferType) -> Option { + match type_.cast_ref::() { + Some(pb_type) => Some(Self::PasteboardType(pb_type.clone())), + None => type_.hint().map(Into::into), + } + } +} + +impl From for PasteboardTypeSpec { + fn from(value: TypeHint) -> Self { + match PasteboardType::from_hint(value) { + Some(pb_type) => Self::PasteboardType(pb_type), + None => Self::TypeHint(value), + } + } +} + +impl PasteboardTypeSpec { + fn pasteboard_type(&self) -> Option<&PasteboardType> { + match self { + PasteboardTypeSpec::PasteboardType(pasteboard_type) => Some(pasteboard_type), + PasteboardTypeSpec::TypeHint(_) => None, + } + } +} + +pub fn dnd_action_to_ns_drag_operation(value: DndAction) -> NSDragOperation { + match value { + DndAction::Copy => NSDragOperation::Copy, + DndAction::Move => NSDragOperation::Move, + DndAction::Link => NSDragOperation::Link, + DndAction::Private => NSDragOperation::Private, + _ => NSDragOperation::empty(), + } +} + +pub fn ns_drag_operation_to_dnd_action(value: NSDragOperation) -> Option { + [ + (NSDragOperation::Copy, DndAction::Copy), + (NSDragOperation::Move, DndAction::Move), + (NSDragOperation::Link, DndAction::Link), + (NSDragOperation::Private, DndAction::Private), + // Sometimes the OS returns `Generic`, in which case we just fall back to `Copy`. + (NSDragOperation::Generic, DndAction::Copy), + ] + .into_iter() + .find_map(|(appkit, winit)| value.contains(appkit).then_some(winit)) +} + +pub fn dnd_actions_to_ns_drag_operation(value: &[DndAction]) -> NSDragOperation { + value + .iter() + .copied() + .map(dnd_action_to_ns_drag_operation) + .fold(NSDragOperation::empty(), BitOr::bitor) +} + +pub fn preferred_drag_operation( + value: NSDragOperation, + preference: &[DndAction], +) -> Option { + preference + .iter() + .find(|action| value.intersects(dnd_action_to_ns_drag_operation(**action))) + .copied() +} + +/// A thin wrapper around [`NSPasteboard`], implementing [`TypedData`]. +#[derive(Debug)] +pub struct PasteboardValue { + // The concept of "top-level" types for a pasteboard doesn't always make sense on macOS due to + // the use of `pasteboardItems`, so we allow using `TypeHint` instead to preserve the user's + // intention. + type_: PasteboardTypeSpec, + pasteboard: Pasteboard, +} + +impl TypedData for PasteboardValue { + fn type_(&self) -> &dyn TransferType { + match &self.type_ { + PasteboardTypeSpec::PasteboardType(pasteboard_type) => { + pasteboard_type as &dyn TransferType + }, + PasteboardTypeSpec::TypeHint(type_hint) => type_hint, + } + } + + fn try_read(&self) -> Option> { + self.try_as_bytes() + .ok() + .map(|bytes| Box::new(io::Cursor::new(bytes)) as Box) + } + + fn try_as_bytes(&self) -> io::Result> { + let type_ = self.type_.clone(); + self.pasteboard + .ns_pasteboard + .get_on_main(|pasteboard| { + let bytes = + pasteboard.dataForType(&NSString::from_str(&type_.pasteboard_type()?.inner))?; + Some(bytes.to_vec()) + }) + .ok_or_else(|| { + io::Error::other(format!( + "NSPasteboard doesn't advertise a binary representation for type {:?}", + self.type_ + )) + }) + } + + fn try_as_uris(&self) -> io::Result> { + // TODO: We should probably use `readObjects`, need to check how that works. + if self.type_().hint() != Some(TypeHint::UriList) { + return Err(io::ErrorKind::InvalidData.into()); + } + + self.pasteboard.ns_pasteboard.get_on_main(|pasteboard| { + let Some(items) = pasteboard.pasteboardItems() else { + // The pasteboard didn't expose any items, so we try with the deprecated method. + #[expect(deprecated)] + let property_list = match pasteboard + .propertyListForType(unsafe { objc2_app_kit::NSFilenamesPboardType }) + { + Some(property_list) => property_list, + None => { + return pasteboard + .stringForType(unsafe { NSPasteboardTypeFileURL }) + .map(|ns_str| vec![ns_str.to_string()]) + .ok_or_else(|| io::ErrorKind::InvalidData.into()); + }, + }; + + let paths = property_list + .downcast::() + .unwrap() + .into_iter() + .map(|file| file.downcast::().unwrap().to_string()) + .collect(); + + return Ok(paths); + }; + + Ok(items + .into_iter() + .filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL })) + .map(|ns_str| ns_str.to_string()) + .collect()) + }) + } + + fn try_as_string(&self) -> io::Result { + let type_ = self.type_.clone(); + + self.pasteboard.ns_pasteboard.get_on_main(|pasteboard| { + pasteboard + .stringForType(&NSString::from_str( + &type_.pasteboard_type().ok_or(io::ErrorKind::InvalidData)?.inner, + )) + .map(|ns_str| ns_str.to_string()) + .ok_or_else(|| io::ErrorKind::InvalidData.into()) + }) + } +} + +#[derive(Debug)] +struct ActivePasteboard { + window_id: WindowId, + pb: MainThreadBound>, +} + +#[derive(Debug, Default)] +pub struct Pasteboards { + inner: RefCell>, +} + +impl Pasteboards { + pub fn remove_deloaded_pasteboards(&self) { + self.inner.borrow_mut().retain(|_, ActivePasteboard { pb, .. }| { + pb.get_on_main(|state| state.load().is_some()) + }); + } + + /// If the data transfer exists, update the pasteboard it points to. + pub fn set_pasteboard( + &self, + id: DataTransferId, + new_pb: &MainThreadBound>, + ) { + let mut inner = self.inner.borrow_mut(); + if let Some(ActivePasteboard { pb, .. }) = inner.get_mut(&id) { + *pb = new_pb.get_on_main(|pb| { + MainThreadBound::new(Weak::from_retained(pb), MainThreadMarker::new().unwrap()) + }); + } + } + + pub fn insert( + &self, + transfer_id: DataTransferId, + pb: &MainThreadBound>, + window_id: WindowId, + ) { + let mut inner = self.inner.borrow_mut(); + let transfer = inner.entry(transfer_id).or_insert_with(move || { + pb.get_on_main(move |pb| ActivePasteboard { + window_id, + pb: MainThreadBound::new(Weak::from_retained(pb), MainThreadMarker::new().unwrap()), + }) + }); + + transfer.window_id = window_id; + } + + pub fn get(&self, id: DataTransferId) -> Option { + self.inner.borrow().get(&id).and_then(|ActivePasteboard { pb, .. }| { + pb.get_on_main(|state| { + let pb = state.load()?; + let pb = MainThreadBound::new(pb, MainThreadMarker::new().unwrap()); + Some(Pasteboard::new(id, pb)) + }) + }) + } + + /// Get the window ID that most-recently saw the provided data transfer. + pub fn window_id(&self, id: DataTransferId) -> Option { + self.inner.borrow().get(&id).map(|active_pasteboard| active_pasteboard.window_id) + } +} + +pub(crate) struct PasteboardWriterState { + data: Box, + // The macOS drag-and-drop API has some confusing aspects when handling multi-drag. The best + // we can really do is have the first element contain all the cross-platform items, and + // any further items are file paths only. + uri: Option>, + writable_types: Retained>, +} + +impl PasteboardWriter { + pub(crate) fn new( + value: Box, + uri: Option>, + ) -> Retained { + let mut writable_types = Vec::>::new(); + value.for_each_available_type(&mut |type_| { + let Some(spec) = PasteboardTypeSpec::from_dyn(type_) else { + return ControlFlow::Continue(()); + }; + + let Some(pb_type) = spec.pasteboard_type() else { + return ControlFlow::Continue(()); + }; + + writable_types.push(NSString::from_str(&pb_type.inner)); + + ControlFlow::Continue(()) + }); + + let pb_writer = Self::alloc().set_ivars(PasteboardWriterState { + data: value, + uri, + writable_types: NSArray::from_retained_slice(&writable_types), + }); + + // Unsure if there's an easier way to do this, but this is how `WindowDelegate` does it. + unsafe { msg_send![super(pb_writer), init] } + } +} + +impl PasteboardWriterState { + fn data_for_pasteboard_type( + &self, + pasteboard_type: &NSPasteboardType, + ) -> Option> { + if pasteboard_type == unsafe { NSPasteboardTypeFileURL } { + if let Some(out) = self.uri.clone().map(Into::into) { + return Some(out); + } + } + let pb_type = PasteboardType::from(pasteboard_type.retain()); + + let mut out = None; + + self.data.for_each_available_type(&mut |haystack| { + if haystack.matches(&pb_type) { + out = self.data.data_for_type(haystack); + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }); + + match out? { + // This should be handled separately + // TODO: Is there a better way to do this? + SendData::Uris(_) => None, + SendData::String(string) => Some(NSString::from_str(&string).into()), + SendData::Bytes(binary) => Some(NSData::from_vec(binary).into()), + } + } +} + +define_class!( + #[unsafe(super(NSObject))] + #[thread_kind = AnyThread] + #[name = "WinitPasteboardWriter"] + #[ivars = PasteboardWriterState] + pub(crate) struct PasteboardWriter; + + unsafe impl NSObjectProtocol for PasteboardWriter {} + + unsafe impl NSPasteboardWriting for PasteboardWriter { + #[unsafe(method_id(writableTypesForPasteboard:))] + fn writable_types_for_pasteboard( + &self, + _: &NSPasteboard, + ) -> Retained> { + let vars = self.ivars(); + vars.writable_types.clone() + } + + #[unsafe(method(writingOptionsForType:pasteboard:))] + fn writing_options_for_type( + &self, + type_: &NSPasteboardType, + pasteboard: &NSPasteboard, + ) -> NSPasteboardWritingOptions { + let _ = type_; + let _ = pasteboard; + NSPasteboardWritingOptions::empty() + } + + #[unsafe(method_id(pasteboardPropertyListForType:))] + fn pasteboard_property_list_for_type( + &self, + type_: &NSPasteboardType, + ) -> Option> { + let vars = self.ivars(); + vars.data_for_pasteboard_type(type_) + } + } +); diff --git a/winit-appkit/src/event_loop.rs b/winit-appkit/src/event_loop.rs index 881762075..a2bc584ee 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -1,30 +1,38 @@ +use std::fmt; use std::rc::Rc; use std::sync::Arc; use std::time::{Duration, Instant}; use objc2::rc::{Retained, autoreleasepool}; use objc2::runtime::ProtocolObject; -use objc2::{MainThreadMarker, available}; +use objc2::{AnyThread, MainThreadMarker, available}; use objc2_app_kit::{ NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification, - NSApplicationWillTerminateNotification, NSWindow, + NSApplicationWillTerminateNotification, NSDraggingItem, NSWindow, }; -use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopCommonModes}; -use objc2_foundation::{NSNotificationCenter, NSObjectProtocol}; +use objc2_core_foundation::{ + CFIndex, CFRunLoopActivity, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes, +}; +use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSString}; use rwh_06::HasDisplayHandle; use tracing::debug_span; use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers}; use winit_common::foundation::create_observer; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, +}; use winit_core::error::{EventLoopError, RequestError}; +use winit_core::event::WindowEvent; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, - EventLoopProxy as CoreEventLoopProxy, OwnedDisplayHandle as CoreOwnedDisplayHandle, + ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents, + DndAction, DragIcon, EventLoopProxy as CoreEventLoopProxy, + OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; -use winit_core::window::Theme; +use winit_core::window::{Theme, WindowId}; use super::app::override_send_event; use super::app_state::AppState; @@ -32,6 +40,8 @@ use super::cursor::CustomCursor; use super::event::dummy_event; use super::monitor; use crate::ActivationPolicy; +use crate::cursor::image_from_icon; +use crate::dnd::{PasteboardTypeSpec, PasteboardWriter, dnd_actions_to_ns_drag_operation}; use crate::window::Window; #[derive(Debug)] @@ -126,8 +136,183 @@ impl RootActiveEventLoop for ActiveEventLoop { fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let Some(pb) = self.app_state.pasteboards().get(id) else { + return Err(RequestError::Ignored); + }; + let Some(window_id) = self.app_state.pasteboards().window_id(id) else { + return Err(RequestError::Ignored); + }; + + let serial = AsyncRequestSerial::get(); + + let Some(type_) = PasteboardTypeSpec::from_dyn(type_) else { + return Err(os_error!(format!("Pasteboard does not contain type {type_:?}")).into()); + }; + + let data = Arc::new(pb.with_type(type_)); + + self.app_state.maybe_queue_with_handler(move |app, event_loop| { + app.window_event(event_loop, window_id, WindowEvent::DataTransferReceived { + id, + serial, + value: data, + }); + }); + + Ok(serial) + } + + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let Some(pb) = self.app_state.pasteboards().get(id) else { + return Err(RequestError::Ignored); + }; + + Ok(Box::new(pb)) + } + + fn set_valid_dnd_actions( + &self, + id: DataTransferId, + actions: &[DndAction], + ) -> Result<(), RequestError> { + let mut state = self.app_state.drag_state().borrow_mut(); + let Some(drag_state) = &mut *state else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + + if drag_state.id != id { + return Err(os_error!(UnknownDataTransfer(id)).into()); + } + + drag_state.valid_actions.clear(); + drag_state.valid_actions.extend_from_slice(actions); + + Ok(()) + } + + fn start_drag( + &self, + source: WindowId, + send_data: Box, + actions: &[DndAction], + icon: Option, + ) -> Result { + let drag_operation = dnd_actions_to_ns_drag_operation(actions); + + self.app_state + .with_window_delegate_on_main(source, move |delegate| { + let (dragging_rect_offset_x, dragging_rect_offset_y) = + icon.as_ref().map(|icon| (icon.offset_x, icon.offset_y)).unwrap_or_default(); + let drag_image = icon.and_then(|icon| image_from_icon(&icon.icon).ok()); + + let Some(event) = delegate.window().currentEvent() else { + return Err(RequestError::Ignored); + }; + + let dragging_rect_size = drag_image + .as_ref() + .map(|img| img.size()) + // Seemingly we need some kind of dragging rectangle even if no icon is + // supplied. + .unwrap_or(CGSize::new(16., 16.)); + + let event_location = event.locationInWindow(); + let dragging_rect_location = CGPoint::new( + event_location.x + dragging_rect_offset_x as f64, + // Convert generic coordinates (y=0 is top of image) to AppKit coordinates (y=0 + // is bottom of image) + event_location.y - dragging_rect_size.height - dragging_rect_offset_y as f64, + ); + let dragging_rect = CGRect::new(dragging_rect_location, dragging_rect_size); + + let mut uris = send_data + .data_for_type(&TypeHint::UriList) + .and_then(|file_uris| { + // TODO: Might not be ideal to do this + let ns_url_from_str = |str: String| NSString::from_str(&str); + // Slightly complicated use of iterators in order to ensure that branches + // have the same opaque type + match file_uris { + SendData::Uris(os_strings) => Some( + None.into_iter().chain(os_strings.into_iter().map(ns_url_from_str)), + ), + SendData::String(string) => Some( + Some(NSString::from_str(&string)) + .into_iter() + .chain(Vec::new().into_iter().map(ns_url_from_str)), + ), + SendData::Bytes(_) => None, + } + }) + .into_iter() + .flatten(); + + let first_uri = uris.next(); + + let mut pasteboard_items = uris + .map(|ns_url| { + let dragging_item = NSDraggingItem::initWithPasteboardWriter( + NSDraggingItem::alloc(), + ProtocolObject::from_ref(&*ns_url), + ); + + // No dragging frame/contents, icon only applies to the first item. + + dragging_item + }) + .collect::>(); + + let first_dragging_item = NSDraggingItem::initWithPasteboardWriter( + NSDraggingItem::alloc(), + ProtocolObject::from_ref(&*PasteboardWriter::new(send_data, first_uri)), + ); + + unsafe { + first_dragging_item.setDraggingFrame_contents( + dragging_rect, + drag_image.as_ref().map(AsRef::as_ref), + ) + }; + + pasteboard_items.insert(0, first_dragging_item); + + let pasteboard_items = NSArray::from_retained_slice(&pasteboard_items); + + let session = delegate.window().beginDraggingSessionWithItems_event_source( + &pasteboard_items, + &event, + ProtocolObject::from_ref(&*delegate), + ); + + let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64); + + delegate.view().set_dragging_session(session, drag_operation); + + Ok(id) + }) + .ok_or(RequestError::Ignored)? + } } +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + impl rwh_06::HasDisplayHandle for ActiveEventLoop { fn display_handle(&self) -> Result, rwh_06::HandleError> { let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new()); diff --git a/winit-appkit/src/lib.rs b/winit-appkit/src/lib.rs index bcec63f14..8548f8f73 100644 --- a/winit-appkit/src/lib.rs +++ b/winit-appkit/src/lib.rs @@ -71,6 +71,7 @@ mod util; mod app; mod app_state; mod cursor; +mod dnd; mod event; mod event_loop; mod ffi; @@ -91,6 +92,7 @@ use winit_core::event_loop::ActiveEventLoop; use winit_core::monitor::MonitorHandle; use winit_core::window::{PlatformWindowAttributes, Window}; +pub use self::dnd::{Pasteboard, PasteboardType, PasteboardValue}; pub use self::event::{physicalkey_to_scancode, scancode_to_physicalkey}; use self::event_loop::ActiveEventLoop as AppKitActiveEventLoop; pub use self::event_loop::{EventLoop, PlatformSpecificEventLoopAttributes}; diff --git a/winit-appkit/src/view.rs b/winit-appkit/src/view.rs index 69b87a49d..d461e77df 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -8,8 +8,9 @@ use objc2::rc::Retained; use objc2::runtime::{AnyObject, Sel}; use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send}; use objc2_app_kit::{ - NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea, - NSTrackingAreaOptions, NSView, NSViewLayerContentsRedrawPolicy, NSWindow, + NSApplication, NSCursor, NSDragOperation, NSDraggingSession, NSEvent, NSEventPhase, + NSResponder, NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView, + NSViewLayerContentsRedrawPolicy, NSWindow, }; use objc2_core_foundation::CGRect; use objc2_foundation::{ @@ -114,6 +115,12 @@ pub struct ViewState { /// Strong reference to the global application state. app_state: Rc, + /// This is for a dragging session that we initiated + dragging_session: RefCell>>, + + /// This is for a dragging session that we initiated + drag_operations: Cell, + cursor_state: RefCell, ime_position: Cell, ime_size: Cell, @@ -789,6 +796,8 @@ impl WinitView { ) -> Retained { let this = mtm.alloc().set_ivars(ViewState { app_state: Rc::clone(app_state), + dragging_session: Default::default(), + drag_operations: Cell::new(NSDragOperation::empty()), cursor_state: Default::default(), ime_position: Default::default(), ime_size: Default::default(), @@ -898,7 +907,7 @@ impl WinitView { } } - fn scale_factor(&self) -> f64 { + pub(crate) fn scale_factor(&self) -> f64 { self.window().backingScaleFactor() as f64 } @@ -1117,6 +1126,29 @@ impl WinitView { self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); } + pub(crate) fn set_dragging_session( + &self, + drag: Retained, + action_mask: NSDragOperation, + ) { + let vars = self.ivars(); + vars.dragging_session.replace(Some(drag)); + vars.drag_operations.set(action_mask); + } + + pub(crate) fn drag_operations(&self) -> NSDragOperation { + self.ivars().drag_operations.get() + } + + pub(crate) fn clear_dragging_session(&self, drag: &NSDraggingSession) -> bool { + let vars = self.ivars(); + vars.drag_operations.set(NSDragOperation::empty()); + let mut dragging_session = vars.dragging_session.borrow_mut(); + dragging_session + .take_if(|session| session.draggingSequenceNumber() == drag.draggingSequenceNumber()) + .is_some() + } + fn mouse_click(&self, event: &NSEvent, button_state: ElementState) { let position = self.mouse_view_point(event).to_physical(self.scale_factor()); let button = mouse_button(event); diff --git a/winit-appkit/src/window.rs b/winit-appkit/src/window.rs index bc6cde65d..c454160c1 100644 --- a/winit-appkit/src/window.rs +++ b/winit-appkit/src/window.rs @@ -36,6 +36,7 @@ impl Window { let mtm = window_target.mtm; let delegate = autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?; + window_target.app_state.register_window(&delegate, mtm); Ok(Window { window: MainThreadBound::new(delegate.window().retain(), mtm), delegate: MainThreadBound::new(delegate, mtm), diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index 4697cf820..52e83cf80 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -6,6 +6,7 @@ use std::ptr; use std::rc::Rc; use std::sync::{Arc, Mutex}; +use dispatch2::MainThreadBound; use dpi::{ LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize, Position, Size, @@ -19,14 +20,14 @@ use objc2::{ use objc2_app_kit::{ NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization, NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType, - NSColor, NSDraggingDestination, NSDraggingInfo, NSRequestUserAttentionType, NSScreen, - NSToolbar, NSView, NSViewFrameDidChangeNotification, NSWindow, NSWindowButton, - NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode, - NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility, - NSWindowToolbarStyle, + NSColor, NSDragOperation, NSDraggingContext, NSDraggingDestination, NSDraggingInfo, + NSDraggingSession, NSDraggingSource, NSPasteboardTypeFileURL, NSPasteboardTypeHTML, + NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF, + NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification, + NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, + NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, + NSWindowTitleVisibility, NSWindowToolbarStyle, }; -#[allow(deprecated)] -use objc2_app_kit::{NSFilenamesPboardType, NSWindowFullScreenButton}; use objc2_core_foundation::{CGFloat, CGPoint}; use objc2_core_graphics::{ CGAcquireDisplayFadeReservation, CGAssociateMouseAndMouseCursorPosition, CGDisplayCapture, @@ -44,6 +45,7 @@ use objc2_foundation::{ use tracing::{debug_span, trace, warn}; use winit_common::core_foundation::MainRunLoop; use winit_core::cursor::Cursor; +use winit_core::data_transfer::DataTransferId; use winit_core::error::{NotSupportedError, RequestError}; use winit_core::event::{SurfaceSizeWriter, WindowEvent}; use winit_core::icon::Icon; @@ -60,6 +62,10 @@ use super::monitor::{self, MonitorHandle, flip_window_screen_coordinates, get_di use super::util::cgerr; use super::view::WinitView; use super::window::{WinitPanel, WinitWindow, window_id}; +use crate::app_state::DragState; +use crate::dnd::{ + dnd_action_to_ns_drag_operation, ns_drag_operation_to_dnd_action, preferred_drag_operation, +}; use crate::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS}; #[derive(Debug)] @@ -358,37 +364,78 @@ define_class!( } } + unsafe impl NSDraggingSource for WindowDelegate { + #[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))] + fn dragging_session_source_operation_mask( + &self, + _: &NSDraggingSession, + _: NSDraggingContext, + ) -> NSDragOperation { + self.view().drag_operations() + } + + #[unsafe(method(draggingSession:endedAtPoint:operation:))] + fn dragging_session_ended_at_point( + &self, + session: &NSDraggingSession, + _: NSPoint, + operation: NSDragOperation, + ) { + let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64); + if operation == NSDragOperation::None { + self.queue_event(WindowEvent::OutgoingDragCanceled { id }); + } else { + self.queue_event(WindowEvent::OutgoingDragDropped { + id, + action: ns_drag_operation_to_dnd_action(operation), + }); + } + + self.view().clear_dragging_session(session); + } + } + unsafe impl NSDraggingDestination for WindowDelegate { /// Invoked when the dragged image enters destination bounds or frame #[unsafe(method(draggingEntered:))] - fn dragging_entered(&self, sender: &ProtocolObject) -> bool { + fn dragging_entered(&self, sender: &ProtocolObject) -> NSDragOperation { let _entered = debug_span!("draggingEntered:").entered(); - use std::path::PathBuf; - - let pb = sender.draggingPasteboard(); - - #[allow(deprecated)] - let property_list = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { - Some(property_list) => property_list, - None => return false.into(), - }; - - let paths = property_list - .downcast::() - .unwrap() - .into_iter() - .map(|file| PathBuf::from(file.downcast::().unwrap().to_string())) - .collect(); + let pb = + MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap()); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); let position = LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()); - self.queue_event(WindowEvent::DragEntered { paths, position }); + let window_id = self.id(); - true + let vars = self.ivars(); + + let source_operations = sender.draggingSourceOperationMask(); + + let transfer_id = DataTransferId::from_raw(sender.draggingSequenceNumber() as i64); + vars.app_state.pasteboards().insert(transfer_id, &pb, window_id); + + vars.app_state + .drag_state() + .replace(Some(DragState { id: transfer_id, valid_actions: vec![] })); + + self.queue_event(WindowEvent::DragEntered { + id: transfer_id, + position: Some(position), + }); + + let drag_state = vars.app_state.drag_state().borrow(); + + drag_state + .as_ref() + .and_then(|drag_state| { + preferred_drag_operation(source_operations, &drag_state.valid_actions) + }) + .map(dnd_action_to_ns_drag_operation) + .unwrap_or(NSDragOperation::empty()) } #[unsafe(method(wantsPeriodicDraggingUpdates))] @@ -400,17 +447,46 @@ define_class!( /// Invoked periodically as the image is held within the destination area, allowing /// modification of the dragging operation or mouse-pointer position. #[unsafe(method(draggingUpdated:))] - fn dragging_updated(&self, sender: &ProtocolObject) -> bool { + fn dragging_updated(&self, sender: &ProtocolObject) -> NSDragOperation { let _entered = debug_span!("draggingUpdated:").entered(); + let vars = self.ivars(); + + let Some(transfer_id) = + vars.app_state.drag_state().borrow().as_ref().map(|state| state.id) + else { + return NSDragOperation::empty(); + }; + + let pb = + MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap()); + + let source_operations = sender.draggingSourceOperationMask(); + + vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb); + let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); let position = LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()); - self.queue_event(WindowEvent::DragMoved { position }); + let proposed_action = vars.app_state.proposed_drag_action(source_operations); - true + self.queue_event(WindowEvent::DragPosition { + id: transfer_id, + position, + proposed_action, + }); + + let drag_state = vars.app_state.drag_state().borrow(); + + drag_state + .as_ref() + .and_then(|drag_state| { + preferred_drag_operation(source_operations, &drag_state.valid_actions) + }) + .map(dnd_action_to_ns_drag_operation) + .unwrap_or(NSDragOperation::empty()) } /// Invoked when the image is released @@ -425,30 +501,43 @@ define_class!( fn perform_drag_operation(&self, sender: &ProtocolObject) -> bool { let _entered = debug_span!("performDragOperation:").entered(); - use std::path::PathBuf; + let vars = self.ivars(); - let pb = sender.draggingPasteboard(); - - #[allow(deprecated)] - let property_list = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { - Some(property_list) => property_list, - None => return false.into(), + let Some(transfer_id) = + vars.app_state.drag_state().borrow().as_ref().map(|state| state.id) + else { + return false.into(); }; - let paths = property_list - .downcast::() - .unwrap() - .into_iter() - .map(|file| PathBuf::from(file.downcast::().unwrap().to_string())) - .collect(); + let pb = + MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap()); + + let source_operations = sender.draggingSourceOperationMask(); + // let operations = ns_drag_operation_to_dnd_actions(source_operations); + + vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb); let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); let position = LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()); - self.queue_event(WindowEvent::DragDropped { paths, position }); + let proposed_action = vars.app_state.proposed_drag_action(source_operations); + self.queue_event(WindowEvent::DragPosition { + id: transfer_id, + position, + proposed_action, + }); + + // Check again, in case the application updated + let proposed_action = vars.app_state.proposed_drag_action(source_operations); + + self.queue_event(WindowEvent::DragDropped { id: transfer_id, proposed_action }); + + // We assume that if the OS has sent `perform_drag_operation`, that the drag succeeded. + // We may want to extend this API in the future to allow signalling that the final drop + // failed. true } @@ -456,6 +545,10 @@ define_class!( #[unsafe(method(concludeDragOperation:))] fn conclude_drag_operation(&self, _sender: Option<&NSObject>) { let _entered = debug_span!("concludeDragOperation:").entered(); + let vars = self.ivars(); + + vars.app_state.pasteboards().remove_deloaded_pasteboards(); + vars.app_state.drag_state().take(); } /// Invoked when the dragging operation is cancelled @@ -463,13 +556,39 @@ define_class!( fn dragging_exited(&self, sender: Option<&ProtocolObject>) { let _entered = debug_span!("draggingExited:").entered(); - let position = sender.map(|sender| { + let vars = self.ivars(); + + let Some(transfer_id) = + vars.app_state.drag_state().borrow().as_ref().map(|state| state.id) + else { + return; + }; + + if let Some(sender) = sender { + let pb = MainThreadBound::new( + sender.draggingPasteboard(), + MainThreadMarker::new().unwrap(), + ); + vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb); + let dl = sender.draggingLocation(); let dl = self.view().convertPoint_fromView(dl, None); - LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()) - }); + let position = + LogicalPosition::::from((dl.x, dl.y)).to_physical(self.scale_factor()); - self.queue_event(WindowEvent::DragLeft { position }); + let source_operations = sender.draggingSourceOperationMask(); + let proposed_action = vars.app_state.proposed_drag_action(source_operations); + + self.queue_event(WindowEvent::DragPosition { + id: transfer_id, + position, + proposed_action, + }); + } + + self.queue_event(WindowEvent::DragLeft { id: transfer_id }); + + vars.app_state.drag_state().take(); } } @@ -674,7 +793,7 @@ fn new_window( if macos_attrs.titlebar_buttons_hidden { for titlebar_button in &[ #[allow(deprecated)] - NSWindowFullScreenButton, + objc2_app_kit::NSWindowFullScreenButton, NSWindowButton::MiniaturizeButton, NSWindowButton::CloseButton, NSWindowButton::ZoomButton, @@ -756,10 +875,6 @@ fn new_window( window.setBackgroundColor(Some(&NSColor::clearColor())); } - // register for drag and drop operations. - #[allow(deprecated)] - window.registerForDraggedTypes(&NSArray::from_slice(&[unsafe { NSFilenamesPboardType }])); - Some(window) }) } @@ -837,6 +952,22 @@ impl WindowDelegate { window.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); + let drag_types = unsafe { + // Advertize support for the set of types which correspond to variants of `TypeHint`. + // If the user wants to support other pasteboard types which don't have a cross-platform + // equivalent, they can downcast the window and manually call `registerForDraggedTypes` + // themselves. + NSArray::from_slice(&[ + NSPasteboardTypeFileURL, + NSPasteboardTypeHTML, + NSPasteboardTypePNG, + NSPasteboardTypeSound, + NSPasteboardTypeString, + NSPasteboardTypeTIFF, + ]) + }; + window.registerForDraggedTypes(&drag_types); + // Listen for theme change event. // // SAFETY: The observer is un-registered in the `Drop` of the delegate. diff --git a/winit-core/Cargo.toml b/winit-core/Cargo.toml index 7b47cc4d3..041c6b9fa 100644 --- a/winit-core/Cargo.toml +++ b/winit-core/Cargo.toml @@ -30,6 +30,7 @@ keyboard-types.workspace = true rwh_06.workspace = true serde = { workspace = true, optional = true } smol_str.workspace = true +url.workspace = true # `wasm32-unknown-unknown` and `wasm32-none`, but not `wasm32-wasi`. [target.'cfg(all(target_family = "wasm", any(target_os = "unknown", target_os = "none")))'.dependencies] diff --git a/winit-core/src/data_transfer.rs b/winit-core/src/data_transfer.rs new file mode 100644 index 000000000..96b060669 --- /dev/null +++ b/winit-core/src/data_transfer.rs @@ -0,0 +1,593 @@ +//! Cross-platform abstractions related to data transfer (i.e. clipboard and drag-and-drop). +//! +//! > **NOTE**: Interacting with the clipboard is currently not implemented in Winit, and +//! > this API is only used for drag-and-drop. +//! +//! # Quickstart +//! +//! The API in this module is used for both sending and receiving data. The flow is detailed below, +//! but to quickly get started, the relevant APIs are the following: +//! +//! ### Receiving a drag-and-drop operation +//! +//! - [`DragEntered`](crate::event::WindowEvent::DragEntered) - informs a window that a new drag +//! operation has started. +//! - [`data_transfer`](crate::event_loop::ActiveEventLoop::data_transfer) - get metadata about the +//! incoming transfer. +//! - [`DataTransfer`] - metadata about the incoming transfer, in particular the available types +//! - [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions) - the +//! application must set at least some actions as valid in order for the drag to be considered +//! accepted. +//! - [`fetch_data_transfer`](crate::event_loop::ActiveEventLoop::fetch_data_transfer) - request the +//! actual data, with a specific type, from the data transfer. +//! - [`DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) - the actual data, +//! with a specific type, has been received. +//! - [`TypedData`] - provides methods to read the actual data +//! +//! ### Sending a drag-and-drop operation +//! +//! - [`DataTransferSend`] - the core trait which defines data to be sent +//! - [`DataTransferSendBuilder`] - helper to create a new outgoing data transfer from a set of +//! types and callbacks that supply data of that type +//! - [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag) - the +//! application calls this to start a new drag operation +//! - [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped)/ +//! [`OutgoingDragCanceled`](crate::event::WindowEvent::OutgoingDragCanceled) - the application +//! receives this when the user has ended the drag operation, by dropping the data or by canceling +//! the operation respectively +//! +//! # Detailed flow +//! +//! ## Receiving a drag-and-drop operation +//! +//! On all platforms, the process looks something like this: +//! +//! - A data transfer advertises a set of types which the data can be interpreted as. While the +//! precise implementation depends on platform, there's a set of types which can be safely +//! transferred between applications on all platforms (see [`TypeHint`]). +//! - For example, if you copy or drag text from a web page, the browser may advertise the text +//! formatted using HTML, the text formatted as RTF, and the text with all formatting removed +//! simultaneously. +//! - An application receiving a data transfer chooses one or more types that it understands and +//! requests the data in those formats (in practice, it will usually only request a single +//! format). +//! - The source application converts the data stored in its memory to the requested format and +//! asynchronously sends it to the target application +//! +//! On some platforms, the data is sometimes available synchronously, but all platforms have at +//! least some method of sending the data asynchronously and some types of data that may _only_ be +//! sent using the asynchronous interface. Because of this, the API in winit must be asynchronous. +//! +//! The flow for a user application that implements drag-and-drop would look something like this: +//! +//! - The application receives a [`DragEntered`](crate::event::WindowEvent::DragEntered) event. This +//! event supplies a [`DataTransferId`] which can be used to request information or operations on +//! the dragged data by using methods on [`Window`](crate::window::Window). +//! - To make sure that the operating system displays the correct cursor, and that modifier keys +//! will change the selected drag action correctly, the application should call +//! [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions). See +//! documentation on that method for details. +//! - As the drag operation continues, the window will receive +//! [`DragPosition`](crate::event::WindowEvent::DragPosition) events. +//! - At any point during this operation, the receiving application may request either the available +//! types or even the data being transferred. This may be useful in cases where the application +//! wants to preload the data. For example, an image editor may want to display the image on the +//! canvas during the drag operation. +//! - When the user tries to drop the data onto the window, that window will receive either a +//! [`DragDropped`](crate::event::WindowEvent::DragDropped) or +//! [`DragLeft`](crate::event::WindowEvent::DragLeft) event if the drag operation was accepted or +//! rejected, respectively. See documentation for +//! [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions) for +//! details on accepting/rejecting a drag. +//! +//! ## Sending a drag-and-drop operation +//! +//! As the source application cannot interact with the ongoing drag while it is in-flight, this flow +//! is a lot simpler. +//! +//! - The application creates a [`DataTransferSend`] with a set of types and associated data. For +//! most cases, this can be done with [`DataTransferSendBuilder`]. +//! - The application passes this [`DataTransferSend`] to +//! [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag)`. This is also +//! where metadata is set, such as the icon that will be shown during the drag operation. +//! - When the drag operation completes, the application receives +//! [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped) with the resultant +//! action, or [`OutgoingDragCanceled`](crate::event::WindowEvent::OutgoingDragCanceled), and +//! handles it appropriately. For example, if the drag was successful and the operation is +//! [`DndAction::Move`](crate::event_loop::DndAction::Move), then the application would delete the +//! source object, since the data has now been transferred somewhere else. + +#![warn(missing_docs)] + +use std::ops::ControlFlow; +use std::path::{Path, PathBuf}; +use std::{fmt, io}; + +use crate::as_any::AsAny; + +/// Unique identifier for a data transfer. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DataTransferId(i64); + +impl DataTransferId { + /// Convert the [`DataTransferId`] into the underlying integer. + /// + /// This is useful if you need to pass the ID across an FFI boundary, or store it in an atomic. + pub const fn into_raw(self) -> i64 { + self.0 + } + + /// Construct a [`DataTransferId`] from the underlying integer. + /// + /// This should only be called with integers returned from [`DataTransferId::into_raw`]. + pub const fn from_raw(id: i64) -> Self { + Self(id) + } +} + +/// The set of types supported cross-platform. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum TypeHint { + /// Plain UTF-8 text (see [`TypedData::try_as_string`]). + /// + /// **Note for platform implementations**: this hint is _only_ for UTF-8 text. If the platform + /// returns plaintext in some format other than UTF-8 by default, a [`TypedData`] + /// implementation marked with this type hint should convert to UTF-8. + Plaintext, + /// A list of URIs in the format defined by the `text/uri-list` MIME type, encoded as UTF-8 (see + /// [`TypedData::try_as_uris`]). + /// + /// **Note for platform implementations**: this hint is _only_ for URIs encoded precisely in the + /// format specified above. If the platform uses a different format, a [`TypedData`] + /// implementation marked with this type hint should convert to that format. + UriList, + /// A HTML-formatted string + Html, + /// An RTF-formatted string + Rtf, + /// Audio + Audio { + /// An optional hint for the encoding of the supplied bytes, specified using the standard + /// file extension for that audio format, lowercase and without the leading `.`. + extension_hint: Option<&'static str>, + }, + /// Image data + Image { + /// An optional hint for the encoding of the supplied bytes, specified using the standard + /// file extension for that image format, lowercase and without the leading `.`. + extension_hint: Option<&'static str>, + }, +} + +impl TypeHint { + /// Check whether the two type hints "match". + /// + /// This is subtly different to direct equality. If one of the types is an image or audio with a + /// `None` extension hint, then the other type just needs to match variant (i.e. image/audio), + /// the extension does not also have to be `None`. + pub fn matches(&self, other: &Self) -> bool { + match (self, other) { + (Self::Plaintext, Self::Plaintext) + | (Self::UriList, Self::UriList) + | (Self::Html, Self::Html) + | (Self::Rtf, Self::Rtf) => true, + + ( + Self::Audio { extension_hint: this_ext }, + Self::Audio { extension_hint: other_ext }, + ) + | ( + Self::Image { extension_hint: this_ext }, + Self::Image { extension_hint: other_ext }, + ) => match (this_ext, other_ext) { + (Some(this_ext), Some(other_ext)) => this_ext == other_ext, + (None, _) | (_, None) => true, + }, + + _ => false, + } + } +} + +/// The type of a data transfer. +/// +/// [`hint`](TransferType::hint) can be called to get the type in +/// a cross-platform format (see [`TypeHint`]) +pub trait TransferType: AsAny + fmt::Debug { + /// Get the cross-platform representation of this type. + /// + /// If this returns `None`, then this is a platform-dependent type that has no cross-platform + /// equivalent. + fn hint(&self) -> Option; + + /// Check whether two dynamically-typed transfer types are equivalent. + // Can't use a `PartialEq` bound because it causes a dependency cycle. + fn matches(&self, other: &dyn TransferType) -> bool; +} + +impl TransferType for TypeHint { + fn hint(&self) -> Option { + Some(*self) + } + + fn matches(&self, other: &dyn TransferType) -> bool { + other.hint().is_some_and(|hint| self.matches(&hint)) + } +} + +impl_dyn_casting!(TransferType); + +// Replicates the cfg for `url::Url::parse` +#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit"))] +fn default_try_as_file_paths(data: &T) -> io::Result> { + data.try_as_uris().and_then(|uris| { + uris.into_iter() + .map(|uri_string| { + Ok(url::Url::parse(&uri_string) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))? + .to_file_path() + .map_err(|()| io::ErrorKind::InvalidData)?) + }) + .collect() + }) +} + +// Replicates the cfg for `url::Url::parse` +// +// It doesn't matter that this is unimplemented on the web, as we don't currently support +// drag-and-drop for web targets and the web platform can't directly access paths anyway. +#[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit")))] +fn default_try_as_file_paths(_: &T) -> io::Result> { + Err(io::ErrorKind::Unsupported.into()) +} + +/// Data that has been fetched from a data transfer +/// +/// ### Blocking +/// +/// Note that this type provides a blocking interface. In cases where reading this type directly on +/// the event loop would cause a deadlock, the backend will make a best-effort attempt to return an +/// error with [`io::ErrorKind::Deadlock`]. For now, the only way to access the data is via blocking +/// on the event loop, so simply retrying the next time an event is received that references the +/// data transfer should be enough to ensure that the data is accessible. +pub trait TypedData: AsAny + fmt::Debug + Send + Sync { + /// The type of this `TypedData`. + fn type_(&self) -> &dyn TransferType; + + /// If this value is readable as bytes, return a reader than can be used to read those bytes. + /// + /// On some platforms, the reader must be driven incrementally upon each + /// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)`. If + /// you don't need to stream the data and just want the bytes in a single buffer, use + /// [`TypedData::try_as_bytes`]. + fn try_read(&self) -> Option>; + + /// If this value is readable as bytes, return those bytes. + /// + /// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called + /// again upon next receiving + /// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) + fn try_as_bytes(&self) -> io::Result> { + let mut reader = self + .try_read() + .ok_or_else(|| io::Error::other("This `TypedData` is not readable as bytes"))?; + + let mut out = Vec::new(); + + reader.read_to_end(&mut out)?; + + Ok(out) + } + + /// Read this value as a list of URIs. + /// + /// If this value is not readable as URIs, return an error. + /// + /// The returned `String`s should be interpreted as URIs conforming to [RFC 3986](https://www.rfc-editor.org/info/rfc3986/). + /// + /// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called + /// again upon next receiving + /// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) + fn try_as_uris(&self) -> io::Result>; + + /// Read this value as a list of paths. + /// + /// This is provided as a convenience method to avoid the need for the user to manually parse + /// the result of [`try_as_uris`](TypedData::try_as_uris). `try_as_uris` should be preferred + /// when the extra complexity is acceptable, as it is more generic. + /// + /// If this value is not readable as URIs, return an error. + /// + /// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called + /// again upon next receiving + /// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) + fn try_as_file_paths(&self) -> io::Result> { + default_try_as_file_paths(self) + } + + /// Read this value as a plain text string. + /// + /// If this value is not readable as a string, return an error. + /// + /// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called + /// again upon next receiving + /// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) + fn try_as_string(&self) -> io::Result; +} + +// Required for `WindowEvent` to implement `PartialEq` - we just implement this on a best-effort +// basis. +impl PartialEq for dyn TypedData { + fn eq(&self, other: &Self) -> bool { + std::ptr::addr_eq(self, other) + } +} + +impl_dyn_casting!(TypedData); + +/// Metadata about a data transfer. This does not allow actually receiving data, as that is an +/// asynchronous operation. To fetch the data from the source application, see +/// [`ActiveEventLoop::fetch_data_transfer`](crate::event_loop::ActiveEventLoop::fetch_data_transfer). +pub trait DataTransfer: AsAny + fmt::Debug { + /// Iterate over each type advertized by this `DataTransfer`. This is just a minor optimization, + /// in most cases you should probably use [`has_type`](DataTransfer::has_type) or + /// [`available_types`](DataTransfer::available_types). + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>, + ); + + /// Display the list of all available types. + /// + /// This is useful if more-complex type matching is required, but for most cases + /// [`has_type`](DataTransfer::has_type) should be used. + fn available_types(&self) -> Vec<&'_ dyn TransferType> { + let mut out = Vec::new(); + + self.for_each_available_type(&mut |ty| { + out.push(ty); + ControlFlow::Continue(()) + }); + + out + } + + /// Check if the supplied type is provided by this [`DataTransfer`]. + /// + /// Supplying a [`TypeHint`] as the type is supported on all platforms, but if some + /// platform-specific type is required then that platform's implementation of `TransferType` can + /// be used. + fn has_type(&self, type_: &dyn TransferType) -> bool { + let mut found = false; + self.for_each_available_type(&mut |haystack| { + if haystack.matches(type_) { + found = true; + ControlFlow::Break(()) + } else { + ControlFlow::Continue(()) + } + }); + + found + } +} + +impl_dyn_casting!(DataTransfer); + +/// Kinds of data that can be sent via a `DataTransfer`. +/// +/// Some kinds of data cannot be represented by just a binary blob in a cross-platform way. +/// File URIs on Windows and macOS are represented as arrays of strings, and strings have +/// different encoding on different platforms. To allow this to be represented, we allow +/// supplying strings and URIs separately from binary blobs. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum SendData { + /// List of URIs. + /// + /// These should conform to [RFC 3986](https://www.rfc-editor.org/info/rfc3986/). + /// If you just want to send file paths, see [`SendData::from_file_paths`]. + /// + /// Note that `SendData` implements `From` and `From>`, but _not_ + /// `From>`, as it is not necessarily obvious to a reader that `Vec` + /// will be interpreted as a URI list. However, it _does_ implement [`From`](url::Url), + /// if you are using the [`url`](https://docs.rs/url/2) crate. + Uris(Vec), + /// String + /// + /// This can also be constructed with the [`From`](std::string::String) implementation. + String(String), + /// Binary blob + /// + /// This can also be constructed with the [`From>`](std::vec::Vec) implementation. + Bytes(Vec), +} + +impl SendData { + /// Create [`SendData::Uris`] from an iterator of [`Path`]s. + /// + /// All paths must be absolute, and on Windows must include either a drive prefix (e.g. `C:\`) + /// or a UNC prefix (`\\`). See documentation for [`url::Url::from_file_path`]. + pub fn from_file_paths(paths: I) -> Option + where + I: IntoIterator, + I::Item: AsRef, + { + // Replicates the cfg for `url::Url::from_file_path` + #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit"))] + fn from_file_paths_impl(paths: I) -> Option + where + I: IntoIterator, + I::Item: AsRef, + { + paths + .into_iter() + .map(url::Url::from_file_path) + .map(|result| result.map(String::from)) + .collect::, ()>>() + .map(SendData::Uris) + .ok() + } + + // Replicates the cfg for `url::Url::from_file_path` + // + // It doesn't matter that this is unimplemented on the web, as we don't currently support + // drag-and-drop for web targets and the web platform can't directly access paths + // anyway. + #[cfg(not(any( + unix, + windows, + target_os = "redox", + target_os = "wasi", + target_os = "hermit" + )))] + fn from_file_paths_impl(_: I) -> Option { + None + } + + from_file_paths_impl(paths) + } +} + +// We monomorphize these `From` implementations instead of making them generic, in order to +// prevent accidentally casting to the wrong type. +impl From for SendData { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From> for SendData { + fn from(value: Vec) -> Self { + Self::Bytes(value) + } +} + +impl From> for SendData { + fn from(value: Vec) -> Self { + Self::Uris(value.into_iter().map(Into::into).collect()) + } +} + +/// Trait for sending data via a data transfer. +/// +/// See [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag) for where +/// this is used. To build an implementation of this trait dynamically in a cross-platform way, use +/// [`DataTransferSendBuilder`]. +pub trait DataTransferSend: DataTransfer + Send { + /// Get the data for the specified type, or `None` if this value does not supply the given data + /// type. + fn data_for_type(&self, type_: &dyn TransferType) -> Option; +} + +impl_dyn_casting!(DataTransferSend); + +type SendDataCallback = Box Option + Send>; + +/// Dynamic builder for an implementation of [`DataTransferSend`]. +/// +/// On all platforms, inter-application data transfer (i.e. clipboard and drag-and-drop) works like +/// so: +/// +/// - The source advertises a set of types that it can transfer. +/// - The destination picks one or more of those types to receive. +/// - The source sends the data for that type. +/// +/// This type abstracts that in a way that allows data to be sent cross-platform. `T` is an optional +/// state value, which allows the user to have a single source of truth for their data, converting +/// it lazily to the requested type. +pub struct DataTransferSendBuilder { + state: T, + types: Vec<(Box, SendDataCallback)>, +} + +impl fmt::Debug for DataTransferSendBuilder +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NewDataTransferBuilder").field("state", &self.state).finish_non_exhaustive() + } +} + +impl DataTransfer for DataTransferSendBuilder +where + T: fmt::Debug + Send + 'static, +{ + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>, + ) { + let _ = self.types.iter().try_for_each(|(ty, _)| func(&**ty)); + } +} + +impl DataTransferSend for DataTransferSendBuilder +where + T: fmt::Debug + Send + 'static, +{ + fn data_for_type(&self, type_: &dyn TransferType) -> Option { + self.data_for_type(type_) + } +} + +impl DataTransferSendBuilder { + /// Create a new [`DataTransferSendBuilder`], with a state value which acts as + /// the single source of truth for the underlying data. + pub fn new(state: T) -> Self { + Self { state, types: vec![] } + } +} + +impl DataTransferSendBuilder { + fn data_for_type(&self, type_: &dyn TransferType) -> Option { + let (_, func) = self.types.iter().find(|(ty, _)| ty.matches(type_))?; + + func(&self.state, type_) + } + + /// Add a callback which converts the builder's state to the given type. In + /// most cases, `type_` will be [`TypeHint`]. + pub fn add_type(&mut self, type_: Ty, func: F) -> &mut Self + where + Ty: TransferType + Send, + F: Fn(&T, &dyn TransferType) -> Option + Send + 'static, + O: Into, + { + self.types + .push((Box::new(type_), Box::new(move |state, ty| func(state, ty).map(Into::into)))); + self + } + + /// Return a new builder, adding a callback which converts the builder's state + /// to the given type. + /// + /// For cross-platform use, `type_` will be [`TypeHint`]. The closure additionally receives + /// a [`TransferType`], which is not necessarily the same as `type_` for the following reasons: + /// + /// - The OS may have multiple types which are equivalent to the supplied type + /// - `TypeHint::Audio` and `TypeHint::Image` with `extension_hint: None` will advertise all + /// supported audio and image formats, in which case the closure may receive a type with an + /// extension chosen by the receiving application. + pub fn with_type(mut self, type_: Ty, func: F) -> Self + where + Ty: TransferType + Send, + F: Fn(&T, &dyn TransferType) -> Option + Send + 'static, + O: Into, + { + self.add_type(type_, func); + self + } +} + +impl DataTransferSendBuilder +where + T: fmt::Debug + Send + 'static, +{ + /// Consume the builder, returning an implementation of [`DataTransferSend`]. + /// + /// Note that this is only provided for explicitness and ergonomics. [`DataTransferSendBuilder`] + /// implements [`DataTransferSend`] and this method is equivalent to [`Box::new`]. + pub fn build(self) -> Box { + Box::new(self) + } +} diff --git a/winit-core/src/event.rs b/winit-core/src/event.rs index cc6bb6cb1..045a40a77 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -2,8 +2,7 @@ use std::cell::LazyCell; use std::cmp::Ordering; use std::f64; -use std::path::PathBuf; -use std::sync::{Mutex, Weak}; +use std::sync::{Arc, Mutex, Weak}; use dpi::{PhysicalPosition, PhysicalSize}; #[cfg(feature = "serde")] @@ -11,8 +10,9 @@ use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use crate::Instant; +use crate::data_transfer::{DataTransferId, TypedData}; use crate::error::RequestError; -use crate::event_loop::AsyncRequestSerial; +use crate::event_loop::{AsyncRequestSerial, DndAction}; use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}; #[cfg(doc)] use crate::window::Window; @@ -75,42 +75,104 @@ pub enum WindowEvent { /// The window has been destroyed. Destroyed, - /// A file drag operation has entered the window. + /// A drag operation has entered the window. + /// + /// The user can use the `id` to read information about the incoming dragged data, and report + /// whether the operation is accepted or rejected back to the operating system (see + /// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`](`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`)). + /// + /// To read the data being dragged, see + /// [`crate::event_loop::ActiveEventLoop::fetch_data_transfer`](`crate::event_loop::ActiveEventLoop::fetch_data_transfer`). DragEntered { - /// List of paths that are being dragged onto the window. - paths: Vec, - /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be - /// negative on some platforms if something is dragged over a window's decorations (title - /// bar, frame, etc). - position: PhysicalPosition, - }, - /// A file drag operation has moved over the window. - DragMoved { - /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be - /// negative on some platforms if something is dragged over a window's decorations (title - /// bar, frame, etc). - position: PhysicalPosition, - }, - /// The file drag operation has dropped file(s) on the window. - DragDropped { - /// List of paths that are being dragged onto the window. - paths: Vec, - /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be - /// negative on some platforms if something is dragged over a window's decorations (title - /// bar, frame, etc). - position: PhysicalPosition, - }, - /// The file drag operation has been cancelled or left the window. - DragLeft { - /// (x,y) coordinates in pixels relative to the top-left corner of the window. May be - /// negative on some platforms if something is dragged over a window's decorations (title - /// bar, frame, etc). + /// ID of the data transfer object, see + /// [`crate::event_loop::ActiveEventLoop::data_transfer`](`crate::event_loop::ActiveEventLoop::data_transfer`). + id: DataTransferId, + /// (x,y) coordinates in pixels relative to the top-left corner of the window. /// - /// ## Platform-specific + /// May be negative on some platforms if something is dragged over a window's decorations + /// (title bar, frame, etc). /// - /// - **Windows:** Always emits [`None`]. + /// Some platforms will provide this on enter, others do not. If + /// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`](`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`) + /// is never called, the default state is for the drag operation to be rejected. The + /// position is provided here when available to allow the application to accept a drag + /// operation as soon as possible, preventing the cursor from flickering from rejected to + /// accepted. position: Option>, }, + /// The position of an ongoing drag operation has changed. + DragPosition { + /// ID of the data transfer object, see + /// [`crate::event_loop::ActiveEventLoop::data_transfer`](`crate::event_loop::ActiveEventLoop::data_transfer`). + id: DataTransferId, + /// (x,y) coordinates in pixels relative to the top-left corner of the window. + /// + /// May be negative on some platforms if something is dragged over a window's decorations + /// (title bar, frame, etc). + position: PhysicalPosition, + /// The drag action proposed by the OS, based on the actions supplied in + /// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`], the actions available on + /// the source, and the held modifier keys. + /// + /// This may be `None` if the backend has not supplied a valid action. On some platforms + /// (in particular, X11), the application is only informed of the proposed action once + /// the operation completes. + proposed_action: Option, + }, + /// A drag operation has dropped file(s) on the window. + DragDropped { + /// ID of the data transfer object, see + /// [`crate::event_loop::ActiveEventLoop::data_transfer`]. + id: DataTransferId, + /// The drag action proposed by the OS, based on the actions supplied in + /// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`], the actions available on + /// the source, and the held modifier keys. + /// + /// This may be `None` if the backend has not supplied a valid action. This is different + /// from the drag being canceled: the drag completed successfully, we just don't know + /// what action was selected. + proposed_action: Option, + }, + /// A drag operation has been canceled or left the window. + DragLeft { + /// ID of the data transfer object, see + /// [`crate::event_loop::ActiveEventLoop::data_transfer`]. + id: DataTransferId, + }, + /// Data is available for a specific fetch request, see + /// [`fetch_data_transfer`](crate::event_loop::ActiveEventLoop::data_transfer). + /// + /// While winit makes a best effort to only send this event precisely once, on some platforms it + /// may not be possible to uniquely determine the window that should receive it. In these + /// cases, winit may dispatch the event to all windows that have access to the data + /// transfer. If your application should only process this event once per data transfer, the + /// `serial` field can be used to deduplicate it. + DataTransferReceived { + /// ID of the data transfer object, see + /// [`crate::event_loop::ActiveEventLoop::data_transfer`]. + id: DataTransferId, + /// Serial returned from `fetch_data_transfer`. + serial: AsyncRequestSerial, + /// The data for the transfer, with a specific type. + value: Arc, + }, + + /// A drag operation started with `start_drag` has been dropped. + OutgoingDragDropped { + /// The ID returned from `start_drag` + id: DataTransferId, + /// The operation selected by the drop destination. + /// + /// This may be `None` if the backend has not supplied a valid action. This is different + /// from the drag being canceled: the drag completed successfully, we just don't know + /// what action was selected. + action: Option, + }, + /// A drag operation started with `start_drag` has been canceled. + OutgoingDragCanceled { + /// The ID returned from `start_drag` + id: DataTransferId, + }, /// The window gained or lost focus. /// @@ -1582,16 +1644,20 @@ mod tests { use crate::event::Ime::Enabled; use crate::event::WindowEvent::*; use crate::event::{PointerKind, PointerSource}; + use crate::event_loop::DndAction; + use crate::data_transfer::DataTransferId; + + let dnd_data = DataTransferId::from_raw(123); with_window_event(CloseRequested); with_window_event(Destroyed); with_window_event(Focused(true)); with_window_event(Moved((0, 0).into())); with_window_event(SurfaceResized((0, 0).into())); - with_window_event(DragEntered { paths: vec!["x.txt".into()], position: (0, 0).into() }); - with_window_event(DragMoved { position: (0, 0).into() }); - with_window_event(DragDropped { paths: vec!["x.txt".into()], position: (0, 0).into() }); - with_window_event(DragLeft { position: Some((0, 0).into()) }); + with_window_event(DragEntered { id: dnd_data, position: None }); + with_window_event(DragPosition { id: dnd_data, position: (0, 0).into(), proposed_action: Some(DndAction::Copy) }); + with_window_event(DragDropped { id: dnd_data, proposed_action: Some(DndAction::Copy) }); + with_window_event(DragLeft { id: dnd_data }); with_window_event(Ime(Enabled)); with_window_event(PointerMoved { device_id: None, diff --git a/winit-core/src/event_loop/mod.rs b/winit-core/src/event_loop/mod.rs index 3ef3ed5f5..06deb1ac6 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -13,9 +13,11 @@ use rwh_06::{DisplayHandle, HandleError, HasDisplayHandle}; use crate::Instant; use crate::as_any::AsAny; use crate::cursor::{CustomCursor, CustomCursorSource}; -use crate::error::RequestError; +use crate::data_transfer::{DataTransfer, DataTransferId, DataTransferSend, TransferType}; +use crate::error::{NotSupportedError, RequestError}; +use crate::icon::Icon; use crate::monitor::MonitorHandle; -use crate::window::{Theme, Window, WindowAttributes}; +use crate::window::{Theme, Window, WindowAttributes, WindowId}; pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Creates an [`EventLoopProxy`] that can be used to dispatch user events @@ -114,8 +116,136 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug { /// Get the raw-window-handle handle. fn rwh_06_handle(&self) -> &dyn HasDisplayHandle; + + /// Request to fetch a type from a [data transfer](crate::data_transfer::DataTransfer). + /// + /// This may be called multiple times on the same [`DataTransferId`] with different types, + /// and may be called at any point during the drag operation, including during handling the + /// [`DragDropped`](crate::event::WindowEvent::DragDropped) event. After that event has been + /// received, though, the data transfer is not guaranteed to be available. The data is + /// _not_ guaranteed to be available during (or after) handling of + /// [`DragLeft](crate::event::WindowEvent::DragLeft). + /// + /// Once available, the data will be supplied to the application with the + /// [`DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) event. + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let _ = id; + let _ = type_; + Err(RequestError::NotSupported(NotSupportedError::new( + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, + ))) + } + + /// Get a [data transfer](DataTransfer) by its ID. + /// + /// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will + /// return an error. + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let _ = id; + Err(RequestError::NotSupported(NotSupportedError::new( + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, + ))) + } + + /// Set a given set of `DndAction`s as the valid actions for the given [`DataTransferId`], + /// if the transfer ID is from an incoming drag-and-drop operation. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. If the data transfer does not exist or is not from a drag-and-drop + /// operation, will return an error. + /// + /// The operating system will consider the drag either accepted or rejected based on the + /// set of valid actions supplied using this method, combined with the set of valid actions + /// on the drag source. If the drag is rejected at the point that the user finalizes the drop, + /// the application will receive [`DragLeft`](crate::event::WindowEvent::DragLeft) instead + /// of [`DragDropped`](crate::event::WindowEvent::DragDropped). + /// + /// Note that _rejecting_ the drag is not the same as _canceling_ the drag. A rejected drag can + /// be accepted later and the user can continue dragging it over other potential targets. On + /// most platforms, there is no way for an application to explicitly cancel a drag + /// operation. + /// + /// The set of actions is expected to be ordered by preference. + fn set_valid_dnd_actions( + &self, + id: DataTransferId, + actions: &[DndAction], + ) -> Result<(), RequestError> { + let _ = id; + let _ = actions; + Err(RequestError::NotSupported(NotSupportedError::new( + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, + ))) + } + + /// Initiate a new drag-and-drop operation. + /// + /// See [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSendBuilder) for how to + /// create a new cross-platform data transfer, or [`DataTransferSend`] for a generic trait + /// which can be implemented manually. + /// + /// The [`DataTransferId`] returned from this method, identifying the outgoing drag, is + /// currently only used for identifying the drag in the + /// [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped) event. In most + /// cases, a drag will be started while the mouse is over the window which started it. This + /// means that, directly after this method is called, the window will then receive a + /// [`DragEntered`](crate::event::WindowEvent::DragEntered) event. However, the ID identifying + /// the incoming drag is not guaranteed to be the same as the ID returned from this method. + /// + /// For most cases, applications can treat all `DragEntered` events the same, whether they were + /// initiated by the same application or a different application. However, if the user wants to + /// have some kind of special handling for internal drag-and-drop, they will currently need + /// to implement it via workaround. On all systems where drag-and-drop is implemented in + /// Winit, the application can make the assumption that only a single drag operation can + /// occur at one time. Therefore, if `DragEntered` is received between calling this method + /// and receiving `OutgoingDragDropped`, then you can assume that it's the same drag. + /// In theory, Wayland allows multiple simultaneous drag operations at a time, but Winit does + /// not currently guarantee that this is supported correctly for either internal or external + /// drag. + /// + /// ### Arguments + /// + /// - `source` - The ID of the window that initiated the drag operation. + /// - `send_data` - The data provided by this drag operation. See + /// [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSendBuilder). + /// - `actions` - The set of valid actions for this drag operation. See [`DndAction`]. On + /// Wayland, this is expected to be ordered by preference. + /// - `icon` - The icon to show while dragging. + /// + /// Some platforms have a more-expressive way of setting the visual component of a drag + /// operation. For those platforms, consider using the platform-specific implementation of + /// [`DataTransferSend`] for `send_data` and set this field to `None`. + /// + /// ### Returns + /// + /// A unique identifier for this drag operation, which will be later supplied by + /// [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped). + fn start_drag( + &self, + source: WindowId, + send_data: Box, + actions: &[DndAction], + icon: Option, + ) -> Result { + let _ = source; + let _ = send_data; + let _ = actions; + let _ = icon; + Err(RequestError::NotSupported(NotSupportedError::new( + DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE, + ))) + } } +const DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE: &str = { + "Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on this \ + platform" +}; + impl HasDisplayHandle for dyn ActiveEventLoop + '_ { fn display_handle(&self) -> Result, HandleError> { self.rwh_06_handle().display_handle() @@ -124,6 +254,73 @@ impl HasDisplayHandle for dyn ActiveEventLoop + '_ { impl_dyn_casting!(ActiveEventLoop); +/// Information needed to initiate a new drag operation. +pub struct DragIcon { + /// The icon to apply to the cursor. + pub icon: Icon, + /// An x offset applied to the dragged icon. + /// + /// This is specified in image pixels. 0 means that the left side of the icon will be at + /// the cursor. + pub offset_x: i32, + /// A y offset applied to the dragged icon. + /// + /// This is specified in image pixels. 0 means that the top of the icon will be at the + /// cursor. + pub offset_y: i32, +} + +impl From for DragIcon { + fn from(value: Icon) -> Self { + Self { icon: value, offset_x: 0, offset_y: 0 } + } +} + +/// The set of available actions for a drag operation. +/// +/// This is _not_ a bitset, as on some platforms (e.g. Wayland, macOS) the source and/or destination +/// are expected to provide some kind of order of preference. +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DndAction { + /// Move the dragged item from the source to the destination. + /// + /// # Platforms + /// + /// - Wayland + /// - macOS + /// - Windows + Move, + /// Copy the dragged item from the source to the destination. + /// + /// # Platforms + /// + /// - X11 + /// - Wayland + /// - macOS + /// - Windows + Copy, + /// A link is established between the source and the destination. + /// + /// # Platforms + /// + /// - macOS + /// - Windows + Link, + /// The user will be prompted for what should be done + /// + /// # Platforms + /// + /// - Wayland + Ask, + /// The source and destination will negotiate the drag operation privately + /// + /// # Platforms + /// + /// - macOS + Private, +} + /// Control the [`ActiveEventLoop`], possibly from a different thread, without referencing it /// directly. #[derive(Clone, Debug)] diff --git a/winit-core/src/lib.rs b/winit-core/src/lib.rs index 89bc68216..2fe5fc82e 100644 --- a/winit-core/src/lib.rs +++ b/winit-core/src/lib.rs @@ -13,6 +13,7 @@ pub mod cursor; #[macro_use] pub mod error; pub mod application; +pub mod data_transfer; pub mod event; pub mod event_loop; pub mod icon; diff --git a/winit-wayland/Cargo.toml b/winit-wayland/Cargo.toml index dfe1fd5d5..b4abd66f9 100644 --- a/winit-wayland/Cargo.toml +++ b/winit-wayland/Cargo.toml @@ -22,6 +22,7 @@ serde = ["dep:serde", "bitflags/serde", "smol_str/serde", "dpi/serde"] bitflags.workspace = true cursor-icon.workspace = true dpi.workspace = true +percent-encoding.workspace = true rwh_06.workspace = true serde = { workspace = true, optional = true } smol_str.workspace = true @@ -40,7 +41,7 @@ sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-feature sctk-adwaita = { version = "0.11.0", default-features = false, optional = true } wayland-backend = { version = "0.3.10", default-features = false, features = ["client_system"] } wayland-client = "0.31.10" -wayland-protocols = { version = "0.32.11", features = ["staging", "unstable"] } +wayland-protocols = { version = "0.32.12", features = ["staging", "unstable"] } wayland-protocols-plasma = { version = "0.3.8", features = ["client"] } winit-common = { workspace = true, features = ["xkb", "wayland"] } diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs new file mode 100644 index 000000000..65243cdfb --- /dev/null +++ b/winit-wayland/src/dnd.rs @@ -0,0 +1,805 @@ +//! Types related to drag-and-drop and data transfer on Wayland. + +use std::ffi::OsStr; +use std::fmt; +use std::io::{self, BufRead, Cursor, ErrorKind, Write}; +use std::ops::{BitOr, Deref}; +use std::sync::Arc; + +use calloop::PostAction; +use dpi::{LogicalPosition, PhysicalPosition}; +use sctk::data_device_manager::WritePipe; +use sctk::data_device_manager::data_device::{DataDeviceData, DataDeviceHandler}; +use sctk::data_device_manager::data_offer::{DataOfferHandler, DragOffer}; +use sctk::data_device_manager::data_source::{DataSourceHandler, DragSource as SctkDragSource}; +use sctk::reexports::client::backend::ObjectId; +use wayland_client::protocol::wl_data_device::WlDataDevice; +use wayland_client::protocol::wl_data_device_manager::DndAction as WlDndAction; +use wayland_client::protocol::wl_data_offer::WlDataOffer; +use wayland_client::protocol::wl_data_source::WlDataSource; +use wayland_client::protocol::wl_surface::WlSurface; +use wayland_client::{Connection, Proxy, QueueHandle}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, +}; +use winit_core::event::WindowEvent; +use winit_core::event_loop::DndAction; +use winit_core::window::WindowId; + +use crate::make_data_transfer_id; +use crate::state::WinitState; + +fn encode_uri_list(uri_list: I) -> Vec +where + I: IntoIterator, + I::Item: AsRef, +{ + let mut out = Vec::new(); + + for uri in uri_list { + out.extend_from_slice(OsStr::new(&uri).as_encoded_bytes()); + out.extend_from_slice(b"\r\n"); + } + + out +} + +impl DataSourceHandler for WinitState { + fn accept_mime( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + _: Option, + ) { + // This method isn't a necessary part of the protocol, it's a holdover from the first + // version of DnD in Wayland and now just serves as a hint. + } + + fn send_request( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + mime: String, + fd: WritePipe, + ) { + let Some(data) = self.dnd_state.send_drag_data_mut() else { + // TODO: Is there a way to explicitly express that the data was not sent? + return; + }; + + let mime = MimeType::parse(mime); + + let Some(send_data) = data.data_for_type(&mime) else { + return; + }; + + let mut encoder = match send_data { + SendData::Uris(strings) => Cursor::new(encode_uri_list(strings)), + SendData::String(str) => match mime.parse_charset() { + Ok(Charset::Utf8) => Cursor::new(str.into_bytes()), + Err(e) => { + tracing::error!("{e}"); + return; + }, + }, + SendData::Bytes(binary) => Cursor::new(binary), + }; + + let _ = self.loop_handle.insert_source(fd, move |_, file, _| { + // Safety: We only mutate `file` in-place and do not replace and drop it. + let file = unsafe { file.get_mut() }; + loop { + let Ok(encoded_bytes) = encoder.fill_buf() else { + return PostAction::Remove; + }; + + match file.write(encoded_bytes) { + Ok(0) => { + break PostAction::Remove; + }, + Ok(consumed) => { + encoder.consume(consumed); + }, + Err(e) if e.kind() == ErrorKind::WouldBlock => { + break PostAction::Continue; + }, + Err(_) => { + break PostAction::Remove; + }, + } + } + }); + } + + fn cancelled(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) { + let Some(current_drag) = self.dnd_state.send_drag() else { + return; + }; + + let window_id = current_drag.window_id; + let id = current_drag.data_transfer_id; + + self.events_sink.push_window_event(WindowEvent::OutgoingDragCanceled { id }, window_id); + } + + fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) { + let Some(current_drag) = self.dnd_state.send_drag() else { + return; + }; + + let window_id = current_drag.window_id; + let id = current_drag.data_transfer_id; + let selected_action = current_drag.selected_action; + + self.events_sink.push_window_event( + WindowEvent::OutgoingDragDropped { + id, + action: dnd_action_wl_to_winit(selected_action), + }, + window_id, + ); + } + + fn dnd_finished( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + self.dnd_state.clear_send_drag(); + } + + fn action( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + action: WlDndAction, + ) { + self.dnd_state.set_target_drag_action(action); + } +} + +#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)] +enum Charset { + #[default] + Utf8, +} + +/// MIME type as string, with an optional hint detected from the MIME type. +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct MimeType { + mime: Arc, + hint: Option, +} + +// MIME types +// Files +const TEXT_URI_LIST: &str = "text/uri-list"; +// Plaintext +const TEXT_PLAIN: &str = "text/plain"; +const TEXT_PLAIN_CHARSET_UTF8: &str = "text/plain; charset=utf-8"; +// HTML +const TEXT_HTML: &str = "text/html"; +const TEXT_HTML_CHARSET_UTF8: &str = "text/html; charset=utf-8"; +// RTF +const APPLICATION_RTF: &str = "application/rtf"; +// Audio +const AUDIO_AAC: &str = "audio/aac"; +const AUDIO_AIFF: &str = "audio/aiff"; +const AUDIO_FLAC: &str = "audio/flac"; +const AUDIO_WAV: &str = "audio/wav"; +const AUDIO_WAVE: &str = "audio/wave"; +const AUDIO_X_WAV: &str = "audio/x-wav"; +const AUDIO_VND_WAV: &str = "audio/vnd.wav"; +const AUDIO_VND_WAVE: &str = "audio/vnd.wave"; +const AUDIO_MPEG: &str = "audio/mpeg"; +const AUDIO_OGG: &str = "audio/ogg"; +// Image +const IMAGE_BMP: &str = "image/bmp"; +const IMAGE_GIF: &str = "image/gif"; +const IMAGE_JPEG: &str = "image/jpeg"; +const IMAGE_PJPEG: &str = "image/pjpeg"; +const IMAGE_PNG: &str = "image/png"; +const IMAGE_SVG: &str = "image/svg+xml"; +const IMAGE_TIFF: &str = "image/tiff"; +const IMAGE_WEBP: &str = "image/webp"; +const IMAGE_X_ICON: &str = "image/x-icon"; +const IMAGE_RAW: &str = "image/x-panasonic-raw"; + +#[derive(Debug)] +struct UnexpectedCharsetError<'a>(&'a str); + +impl std::error::Error for UnexpectedCharsetError<'_> {} + +impl fmt::Display for UnexpectedCharsetError<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unsupported charset: {}", self.0) + } +} + +impl MimeType { + const MIME_HINT_MAP: &[(&str, TypeHint)] = &[ + // Files + (TEXT_URI_LIST, TypeHint::UriList), + // Plaintext + (TEXT_PLAIN, TypeHint::Plaintext), + (TEXT_PLAIN_CHARSET_UTF8, TypeHint::Plaintext), + // HTML + (TEXT_HTML, TypeHint::Html), + (TEXT_HTML_CHARSET_UTF8, TypeHint::Html), + // RTF + (APPLICATION_RTF, TypeHint::Rtf), + // Audio + (AUDIO_AAC, TypeHint::Audio { extension_hint: Some("aac") }), + (AUDIO_AIFF, TypeHint::Audio { extension_hint: Some("aif") }), + (AUDIO_FLAC, TypeHint::Audio { extension_hint: Some("flac") }), + (AUDIO_VND_WAV, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_VND_WAVE, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_WAV, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_WAVE, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_X_WAV, TypeHint::Audio { extension_hint: Some("wav") }), + (AUDIO_OGG, TypeHint::Audio { extension_hint: Some("ogg") }), + (AUDIO_MPEG, TypeHint::Audio { extension_hint: Some("mp3") }), + // Image + (IMAGE_BMP, TypeHint::Image { extension_hint: Some("bmp") }), + (IMAGE_GIF, TypeHint::Image { extension_hint: Some("gif") }), + (IMAGE_JPEG, TypeHint::Image { extension_hint: Some("jpg") }), + (IMAGE_PJPEG, TypeHint::Image { extension_hint: Some("jpg") }), + (IMAGE_PNG, TypeHint::Image { extension_hint: Some("png") }), + (IMAGE_RAW, TypeHint::Image { extension_hint: Some("raw") }), + (IMAGE_SVG, TypeHint::Image { extension_hint: Some("svg") }), + (IMAGE_TIFF, TypeHint::Image { extension_hint: Some("tiff") }), + (IMAGE_WEBP, TypeHint::Image { extension_hint: Some("webp") }), + (IMAGE_X_ICON, TypeHint::Image { extension_hint: Some("ico") }), + ]; + + // Returns an iterator so that things like the multiple charsets for plaintext/HTML + // and the multiple ways of expressing .wav work correctly. + pub(crate) fn from_dyn(type_: &dyn TransferType) -> impl Iterator { + let downcast = type_.cast_ref::().cloned(); + let downcast_failed = downcast.is_none(); + // This filter is a bit hacky, but it's the only way to ensure that we always + // return the same type. + let from_hint = downcast_failed + .then_some( + Self::MIME_HINT_MAP + .iter() + .filter(move |(_, haystack)| TransferType::matches(haystack, type_)) + .map(move |(mime, _)| Self { + mime: mime.to_string().into(), + hint: type_.hint(), + }), + ) + .into_iter() + .flatten(); + + downcast.into_iter().chain(from_hint) + } + + // TODO: We should properly parse MIME types using `mime` or a similar crate. + fn parse_charset(&self) -> Result> { + let Some((_, charset)) = self + .mime + .split_once(';') + .and_then(|(_essence, options)| options.split_once("charset=")) + else { + return Ok(Default::default()); + }; + + let charset = charset.split_once(',').map(|(first, _)| first).unwrap_or(charset).trim(); + + if charset == "utf-8" { Ok(Charset::Utf8) } else { Err(UnexpectedCharsetError(charset)) } + } + + fn parse(mime: String) -> Self { + let hint = Self::MIME_HINT_MAP + .iter() + .find_map(|(haystack, hint)| (*haystack == &*mime).then_some(*hint)) + .or_else(|| { + if mime.starts_with("image/") { + Some(TypeHint::Image { extension_hint: None }) + } else if mime.starts_with("audio/") { + Some(TypeHint::Audio { extension_hint: None }) + } else { + None + } + }); + + Self { mime: mime.into(), hint } + } +} + +impl fmt::Display for MimeType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.mime.fmt(f) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownTypeHint(pub TypeHint); + +impl fmt::Display for UnknownTypeHint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unknown type hint: {:?}", self.0) + } +} + +impl TryFrom for MimeType { + type Error = UnknownTypeHint; + + fn try_from(hint: TypeHint) -> Result { + let mime = Self::MIME_HINT_MAP + .iter() + .find_map(|(mime, haystack)| (*haystack == hint).then_some(*mime)) + .ok_or(UnknownTypeHint(hint))?; + + Ok(Self { mime: mime.to_owned().into(), hint: Some(hint) }) + } +} + +impl TransferType for MimeType { + fn hint(&self) -> Option { + self.hint + } + + fn matches(&self, other: &dyn TransferType) -> bool { + if let Some(other_mime) = other.cast_ref::() { + *self == *other_mime + } else { + // If either hint is `None`, return false + self.hint().is_some_and(|hint| other.hint() == Some(hint)) + } + } +} + +type BytesResult = Result, Arc>; + +/// Typed data transfer from another application. +#[derive(Debug)] +pub struct MimeData { + mime_type: MimeType, + result: BytesResult, +} + +impl MimeData { + pub(crate) fn new(mime_type: MimeType, result: BytesResult) -> Self { + Self { mime_type, result } + } + + fn data(&self) -> io::Result<&[u8]> { + fn arc_to_io_error(arc: Arc) -> io::Error { + io::Error::new(arc.kind(), arc) + } + + self.result.as_deref().map_err(|e| arc_to_io_error(e.clone())) + } +} + +impl TypedData for MimeData { + fn type_(&self) -> &dyn TransferType { + &self.mime_type + } + + fn try_read(&self) -> Option> { + let data = self.data().ok()?.to_owned(); + + Some(Box::new(io::Cursor::new(data))) + } + + fn try_as_bytes(&self) -> io::Result> { + self.data().map(ToOwned::to_owned) + } + + fn try_as_uris(&self) -> io::Result> { + let data = self.data()?; + + Cursor::new(&data) + .lines() + .filter(|result| match result { + Ok(s) => !s.starts_with('#'), + // We want to maintain errors, so the final `collect` returns an error too + Err(_) => true, + }) + .collect() + } + + fn try_as_string(&self) -> io::Result { + let charset = self.mime_type.parse_charset(); + + let data = self.data()?; + + match charset { + Ok(Charset::Utf8) => String::from_utf8(data.to_vec()) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)), + Err(e) => Err(io::Error::other(e.to_string())), + } + } +} + +/// A wrapper around `WlDataOffer`, implementing `DataTransfer`. +#[derive(Debug, Clone)] +pub struct DataOffer { + mime_types: Arc<[MimeType]>, + data: WlDataOffer, + available_actions: WlDndAction, + data_device_id: ObjectId, + serial: u32, + window_id: WindowId, +} + +pub(crate) fn dnd_action_winit_to_wl(winit: DndAction) -> WlDndAction { + match winit { + DndAction::Move => WlDndAction::Move, + DndAction::Copy => WlDndAction::Copy, + DndAction::Ask => WlDndAction::Ask, + _ => WlDndAction::empty(), + } +} + +pub(crate) fn dnd_action_wl_to_winit(wl: WlDndAction) -> Option { + match wl { + WlDndAction::Move => Some(DndAction::Move), + WlDndAction::Copy => Some(DndAction::Copy), + WlDndAction::Ask => Some(DndAction::Ask), + _ => None, + } +} + +impl DataOffer { + pub(crate) fn transfer_id(&self) -> DataTransferId { + make_data_transfer_id(self.data_device_id.clone(), self.serial) + } + + pub(crate) fn first_mime_type(&self) -> Option<&MimeType> { + self.mime_types.first() + } + + pub(crate) fn serial(&self) -> u32 { + self.serial + } + + pub(crate) fn window_id(&self) -> WindowId { + self.window_id + } + + pub(crate) fn set_actions(&self, action_set: &[DndAction]) -> bool { + let preferred_action = action_set.iter().find_map(|winit| { + let wl = dnd_action_winit_to_wl(*winit); + self.available_actions.intersects(wl).then_some(wl) + }); + + let any = preferred_action.is_some(); + + let all_actions = action_set + .iter() + .copied() + .map(dnd_action_winit_to_wl) + .fold(WlDndAction::empty(), BitOr::bitor); + + self.data.set_actions(all_actions, preferred_action.unwrap_or(WlDndAction::empty())); + + any + } + + pub(crate) fn find_type_dyn<'a>(&'a self, type_: &'a dyn TransferType) -> Option<&'a MimeType> { + match type_.cast_ref::() { + Some(mime_type) => Some(mime_type), + None => { + let hint = type_.hint()?; + self.mime_types.iter().find(|mime_type| { + mime_type.hint().is_some_and(|haystack| haystack.matches(&hint)) + }) + }, + } + } +} + +impl Deref for DataOffer { + type Target = WlDataOffer; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DataTransfer for DataOffer { + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, + ) { + let _ = self.mime_types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func); + } +} + +/// Wrapper for [`WlDataSource`], which exposes the types that are advertised by a data +/// transfer operation, along with the data that the source represents +#[derive(Debug)] +pub struct DragSource { + pub(crate) data_transfer_id: DataTransferId, + /// The `WlDataSource` generated from `data`. + /// + /// This is stored internally, as if this source is dropped then the + /// drag operation will be cancelled. + _data_source: SctkDragSource, + /// The supplied [`DataTransferSend`]. + pub(crate) data: Box, + pub(crate) selected_action: WlDndAction, + pub(crate) window_id: WindowId, + /// (Optionally) an icon for the drag-and-drop operation. + _icon: Option, +} + +impl DragSource { + pub(crate) fn new( + data_transfer_id: DataTransferId, + data_source: SctkDragSource, + data: Box, + icon: Option, + window_id: WindowId, + ) -> Self { + Self { + data_transfer_id, + _data_source: data_source, + data, + selected_action: WlDndAction::None, + window_id, + _icon: icon, + } + } + + /// Per-type data to be sent. See [`DataTransferSend`]. + pub fn data(&mut self) -> &mut dyn DataTransferSend { + &mut *self.data + } +} + +/// The current state of an in-progress drag-and-drop operation. +#[derive(Debug, Default)] +pub struct DndState { + receive_drag: Option, + send_drag: Option, +} + +impl DndState { + pub(crate) fn receive_drag(&self) -> Option<&DataOffer> { + self.receive_drag.as_ref() + } + + pub(crate) fn set_send_drag(&mut self, source: DragSource) { + self.send_drag = Some(source); + } + + pub(crate) fn send_drag(&self) -> Option<&DragSource> { + self.send_drag.as_ref() + } + + pub(crate) fn set_target_drag_action(&mut self, action: WlDndAction) { + if let Some(source) = &mut self.send_drag { + source.selected_action = action; + } + } + + /// Returns `true` if a drag operation was in progress, `false` if no drag operation was in + /// progress. + pub(crate) fn clear_send_drag(&mut self) -> bool { + self.send_drag.take().is_some() + } + + pub(crate) fn send_drag_data_mut(&mut self) -> Option<&mut dyn DataTransferSend> { + self.send_drag.as_mut().map(|send| send.data()) + } +} + +impl DataOfferHandler for WinitState { + fn source_actions( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut DragOffer, + actions: WlDndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } + + fn selected_action( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut DragOffer, + actions: WlDndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + } +} + +impl DataDeviceHandler for WinitState { + fn enter( + &mut self, + _: &Connection, + _: &QueueHandle, + data_device: &WlDataDevice, + x: f64, + y: f64, + wl_surface: &WlSurface, + ) { + let Some(data) = data_device.data::() else { + return; + }; + + let Some(drag) = data.drag_offer() else { + // Selections are not yet implemented + return; + }; + + let window_id = crate::make_wid(wl_surface); + + let current_drag = drag.with_mime_types(|types| DataOffer { + mime_types: types + .iter() + .map(|str| MimeType::parse(str.clone())) + .collect::>() + .into(), + available_actions: drag.source_actions, + serial: drag.serial, + data_device_id: data_device.id(), + data: drag.inner().clone(), + window_id, + }); + + current_drag.set_actions(&[]); + + let id = current_drag.transfer_id(); + + self.dnd_state.receive_drag = Some(current_drag); + + let scale_factor = self + .windows + .borrow() + .get(&window_id) + .map(|window| window.lock().unwrap().scale_factor()) + .unwrap_or(1.); + let position: PhysicalPosition = LogicalPosition::new(x, y).to_physical(scale_factor); + + self.events_sink.push_window_event( + WindowEvent::DragEntered { id, position: Some(position) }, + window_id, + ); + } + + fn leave(&mut self, _: &Connection, _: &QueueHandle, data_device: &WlDataDevice) { + let Some(data) = data_device.data::() else { + return; + }; + + if let Some(current_drag) = self.dnd_state.receive_drag() { + self.events_sink.push_window_event( + WindowEvent::DragLeft { id: current_drag.transfer_id() }, + current_drag.window_id(), + ); + + if let Some(receive_drag) = self.dnd_state.receive_drag.take() { + receive_drag.finish(); + } + } + + if let Some(drag) = data.drag_offer() { + drag.destroy(); + } + if let Some(selection) = data.selection_offer() { + selection.destroy(); + } + } + + fn motion( + &mut self, + _: &Connection, + _: &QueueHandle, + data_device: &WlDataDevice, + x: f64, + y: f64, + ) { + let Some(data) = data_device.data::() else { + return; + }; + let Some(drag) = data.drag_offer() else { + // Selections (copy/paste) are not yet implemented + return; + }; + + // `selected_action` should only contain a single flag, but we check with `contains` + // just in case we or the compositor misunderstood the spec. + let proposed_action = if drag.selected_action.contains(WlDndAction::Move) { + Some(DndAction::Move) + } else if drag.selected_action.contains(WlDndAction::Copy) { + Some(DndAction::Copy) + } else if drag.selected_action.contains(WlDndAction::Ask) { + Some(DndAction::Ask) + } else { + None + }; + + let Some(current_drag) = self.dnd_state.receive_drag() else { + return; + }; + + let window_id = crate::make_wid(&drag.surface); + + let scale_factor = self + .windows + .borrow() + .get(&window_id) + .map(|window| window.lock().unwrap().scale_factor()) + .unwrap_or(1.); + let position: PhysicalPosition = LogicalPosition::new(x, y).to_physical(scale_factor); + + self.events_sink.push_window_event( + WindowEvent::DragPosition { id: current_drag.transfer_id(), position, proposed_action }, + window_id, + ); + } + + fn selection(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataDevice) { + // We don't handle selections right now. + } + + fn drop_performed( + &mut self, + _: &Connection, + _: &QueueHandle, + data_device: &WlDataDevice, + ) { + let Some(data) = data_device.data::() else { + return; + }; + let Some(drag) = data.drag_offer() else { + // Selections (copy/paste) are not yet implemented + return; + }; + + let Some(current_drag) = self.dnd_state.receive_drag() else { + return; + }; + + let window_id = crate::make_wid(&drag.surface); + + // `selected_action` should only contain a single flag, but we check with `contains` + // just in case we or the compositor misunderstood the spec. + let proposed_action = if drag.selected_action.contains(WlDndAction::Move) { + Some(DndAction::Move) + } else if drag.selected_action.contains(WlDndAction::Copy) { + Some(DndAction::Copy) + } else if drag.selected_action.contains(WlDndAction::Ask) { + Some(DndAction::Ask) + } else { + None + }; + + self.events_sink.push_window_event( + WindowEvent::DragDropped { id: current_drag.transfer_id(), proposed_action }, + window_id, + ); + + if let Some(receive_drag) = self.dnd_state.receive_drag.take() { + receive_drag.finish(); + } + + if let Some(drag) = data.drag_offer() { + drag.destroy(); + } + if let Some(selection) = data.selection_offer() { + selection.destroy(); + } + } +} + +sctk::delegate_data_device!(WinitState); diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 3a6ca6982..04fbb2a6c 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -1,35 +1,46 @@ //! The event-loop routines. use std::cell::{Cell, RefCell}; -use std::io::Result as IOResult; -use std::mem; +use std::io::{self, Read, Result as IOResult}; +use std::ops::BitOr; use std::os::fd::OwnedFd; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Condvar, Mutex}; use std::thread::JoinHandle; use std::time::{Duration, Instant}; +use std::{fmt, mem}; +use calloop::PostAction; use calloop::ping::Ping; use dpi::LogicalSize; use rustix::event::{PollFd, PollFlags}; use rustix::pipe::{self, PipeFlags}; +use sctk::data_device_manager::{ReadPipe, data_offer}; use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::{Connection, QueueHandle, globals}; +use sctk::shell::WaylandSurface; use tracing::warn; +use wayland_client::Proxy; +use wayland_client::protocol::wl_data_device_manager::DndAction as WlDndAction; +use wayland_client::protocol::wl_shm::Format; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, DataTransferSend, TransferType}; use winit_core::error::{EventLoopError, NotSupportedError, OsError, RequestError}; use winit_core::event::{DeviceEvent, StartCause, SurfaceSizeWriter, WindowEvent}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, - OwnedDisplayHandle as CoreOwnedDisplayHandle, + ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents, + DndAction, DragIcon, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; +use winit_core::icon::RgbaIcon; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::Theme; +use crate::dnd::{MimeData, dnd_action_winit_to_wl}; use crate::types::cursor::WaylandCustomCursor; +use crate::{DragSource, MimeType, image_to_buffer, make_data_transfer_id}; mod proxy; pub mod sink; @@ -678,8 +689,233 @@ impl RootActiveEventLoop for ActiveEventLoop { fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let state = self.state.borrow_mut(); + let Some(current_drag) = state.dnd_state.receive_drag() else { + return Err(RequestError::Ignored); + }; + + if current_drag.transfer_id() != id { + return Err(RequestError::Ignored); + } + + let Some(mime_type) = current_drag.find_type_dyn(type_) else { + return Err(RequestError::Ignored); + }; + + let mime_type_str = mime_type.to_string(); + + // create a pipe + let (readfd, writefd) = + pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK).map_err(|e| os_error!(e))?; + + let async_request_serial = AsyncRequestSerial::get(); + + let mut buffer = Vec::new(); + let window_id = current_drag.window_id(); + let mut mime_type = Some(mime_type.clone()); + + let _ = state.loop_handle.insert_source(ReadPipe::from(readfd), move |_, file, state| { + // SAFETY: We do not overwrite the referent of `file` + let file = unsafe { file.get_mut() }; + + let result = match file.read_to_end(&mut buffer) { + Err(e) if e.kind() == io::ErrorKind::WouldBlock => { + return PostAction::Continue; + }, + Ok(0) => Ok(mem::take(&mut buffer)), + Ok(_) => { + return PostAction::Continue; + }, + Err(e) => Err(Arc::new(e)), + }; + + state.events_sink.push_window_event( + WindowEvent::DataTransferReceived { + id, + serial: async_request_serial, + // `unwrap` is safe here, as we always return `PostAction::Remove` in this + // branch. + value: Arc::new(MimeData::new(mime_type.take().unwrap(), result)), + }, + window_id, + ); + + PostAction::Remove + }); + + current_drag.accept(current_drag.serial(), Some(mime_type_str.clone())); + data_offer::receive_to_fd(current_drag, mime_type_str, writefd); + + Ok(async_request_serial) + } + + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let state = self.state.borrow(); + let Some(state) = state.dnd_state.receive_drag() else { + return Err(RequestError::Ignored); + }; + + if state.transfer_id() != id { + return Err(RequestError::Ignored); + } + + Ok(Box::new(state.clone())) + } + + fn set_valid_dnd_actions( + &self, + id: DataTransferId, + actions: &[DndAction], + ) -> Result<(), RequestError> { + let state = self.state.borrow(); + let Some(state) = state.dnd_state.receive_drag() else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + + if state.transfer_id() != id { + return Err(os_error!(UnknownDataTransfer(id)).into()); + } + + let any_actions = state.set_actions(actions); + let accepted_type = + if any_actions { state.first_mime_type().map(|mime| mime.to_string()) } else { None }; + // Some compositors won't even send the "dropped" event if no type + // has been accepted, so we need to accept _something_ here. The + // application can accept further types by fetching the data, but + // this will at least mean that waiting until the drop to start + // fetching data won't prevent the drop from working at all. + state.accept(state.serial(), accepted_type); + + Ok(()) + } + + fn start_drag( + &self, + source: WindowId, + send_data: Box, + action_mask: &[DndAction], + icon: Option, + ) -> Result { + const NO_POINTER_CAP_ERROR_MSG: &str = + "Tried to initiate drag, but source window does not have the pointer capability"; + + let mut state = self.state.borrow_mut(); + let dnd_actions = action_mask + .iter() + .copied() + .map(dnd_action_winit_to_wl) + .fold(WlDndAction::empty(), BitOr::bitor); + + let data_device_manager = state + .data_device_manager_state + .as_ref() + .ok_or(NotSupportedError::new("Tried to initiate drag, but data device not enabled"))?; + + let mut mime_types = Vec::new(); + send_data.for_each_available_type(&mut |ty_| { + for mime in MimeType::from_dyn(ty_) { + mime_types.push(mime); + } + + std::ops::ControlFlow::Continue(()) + }); + + let data_source = data_device_manager.create_drag_and_drop_source( + &self.queue_handle, + mime_types, + dnd_actions, + ); + + let icon_surface = { + let mut pool = state.image_pool.lock().unwrap(); + icon.and_then(|icon| { + let rgba = icon.icon.cast_ref::()?; + + let width = rgba.width().try_into().ok()?; + let height = rgba.height().try_into().ok()?; + + let buffer = + image_to_buffer(width, height, rgba.buffer(), Format::Argb8888, &mut pool) + .ok()?; + + let surface = state.compositor_state.create_surface(&self.queue_handle); + if surface.version() >= 5 { + buffer.attach_to(&surface).ok()?; + surface.offset(icon.offset_x, icon.offset_y); + } else { + surface.attach(Some(buffer.wl_buffer()), icon.offset_x, icon.offset_y); + } + + Some(surface) + }) + }; + + // New scope to ensure we drop the locks as soon as possible. + let transfer_id = { + let windows = state.windows.borrow(); + let source_window_mutex = windows + .get(&source) + .ok_or(os_error!("Tried to initiate drag, but source window ID was invalid"))?; + let source_window_state = source_window_mutex.lock().unwrap(); + let source_surface = source_window_state.window.wl_surface(); + + let seat = source_window_state + .focused_seats() + .find_map(|seat_id| { + // HACK: How do we get the correct seat for pointers here? + state.seats.get(seat_id).filter(|seat| seat.data_device().is_some()) + }) + .ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?; + let data_device = + seat.data_device().ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?; + + let serial = seat + .pointer_data() + .ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))? + .latest_button_serial(); + + data_source.start_drag(data_device, source_surface, icon_surface.as_ref(), serial); + + make_data_transfer_id(data_device.inner().id(), serial) + }; + + // For some reason, if we commit before starting the drag then the offset isn't applied. + // This doesn't seem to be documented anywhere, and it's possible that it's a bug in KDE. + if let Some(surface) = &icon_surface { + surface.commit(); + } + + state.dnd_state.set_send_drag(DragSource::new( + transfer_id, + data_source, + send_data, + icon_surface, + source, + )); + + Ok(transfer_id) + } } +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + impl ActiveEventLoop { fn clear_exit(&self) { self.exit.set(None) diff --git a/winit-wayland/src/lib.rs b/winit-wayland/src/lib.rs index 930562b38..52ff28885 100644 --- a/winit-wayland/src/lib.rs +++ b/winit-wayland/src/lib.rs @@ -18,13 +18,16 @@ #![allow(clippy::mutable_key_type)] use std::ffi::c_void; +use std::hash::BuildHasher; use std::ptr::NonNull; use dpi::{LogicalSize, PhysicalSize}; use sctk::reexports::client::Proxy; +use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_surface::WlSurface; use sctk::shm::slot::{Buffer, CreateBufferError, SlotPool}; use wayland_client::protocol::wl_shm::Format; +use winit_core::data_transfer::DataTransferId; use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop; use winit_core::window::{ ActivationToken, PlatformWindowAttributes, Window as CoreWindow, WindowId, @@ -34,6 +37,7 @@ macro_rules! os_error { ($error:expr) => {{ winit_core::error::OsError::new(line!(), file!(), $error) }}; } +mod dnd; mod event_loop; mod output; mod seat; @@ -41,6 +45,7 @@ mod state; mod types; mod window; +pub use self::dnd::{DataOffer, DragSource, MimeData, MimeType}; pub use self::event_loop::{ActiveEventLoop, EventLoop}; pub use self::window::Window; @@ -149,6 +154,17 @@ fn make_wid(surface: &WlSurface) -> WindowId { WindowId::from_raw(surface.id().as_ptr() as usize) } +/// Create a `DataTransferId` for the given data device and serial. +/// +/// It's currently unclear if this will result in the same ID when transferring to the same +/// application. +#[inline] +fn make_data_transfer_id(data_device_id: ObjectId, serial: u32) -> DataTransferId { + const BUILD_HASHER: foldhash::fast::FixedState = foldhash::fast::FixedState::with_seed(0); + + DataTransferId::from_raw(BUILD_HASHER.hash_one((data_device_id, serial)) as i64) +} + /// The default routine does floor, but we need round on Wayland. fn logical_to_physical_rounded(size: LogicalSize, scale_factor: f64) -> PhysicalSize { let width = size.width as f64 * scale_factor; diff --git a/winit-wayland/src/seat/mod.rs b/winit-wayland/src/seat/mod.rs index 0ed9a6ffa..cef665974 100644 --- a/winit-wayland/src/seat/mod.rs +++ b/winit-wayland/src/seat/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use foldhash::HashMap; +use sctk::data_device_manager::data_device::DataDevice; use sctk::reexports::client::backend::ObjectId; use sctk::reexports::client::protocol::wl_seat::WlSeat; use sctk::reexports::client::protocol::wl_touch::WlTouch; @@ -64,6 +65,9 @@ pub struct WinitSeatState { /// The hold pointer gesture bound on the seat. pointer_gesture_hold: Option, + /// The drag-and-drop state + data_device: Option, + /// The keyboard bound on the seat. keyboard_state: Option, @@ -78,6 +82,14 @@ impl WinitSeatState { pub fn new() -> Self { Default::default() } + + pub(crate) fn data_device(&self) -> Option<&DataDevice> { + self.data_device.as_ref() + } + + pub(crate) fn pointer_data(&self) -> Option<&WinitPointerData> { + self.pointer.as_ref().and_then(|pointer| pointer.pointer().data()) + } } impl SeatHandler for WinitState { @@ -129,6 +141,11 @@ impl SeatHandler for WinitState { ) .expect("failed to create pointer with present capability."); + seat_state.data_device = self + .data_device_manager_state + .as_ref() + .map(|device| device.get_data_device(queue_handle, &seat)); + seat_state.relative_pointer = self.relative_pointer.as_ref().map(|manager| { manager.get_relative_pointer( themed_pointer.pointer(), @@ -225,6 +242,8 @@ impl SeatHandler for WinitState { pointer_gesture_hold.destroy(); } + seat_state.data_device = None; + if let Some(pointer) = seat_state.pointer.take() { let pointer_data = pointer.pointer().winit_data(); diff --git a/winit-wayland/src/seat/pointer/mod.rs b/winit-wayland/src/seat/pointer/mod.rs index 11970d706..6e9aa2009 100644 --- a/winit-wayland/src/seat/pointer/mod.rs +++ b/winit-wayland/src/seat/pointer/mod.rs @@ -22,19 +22,19 @@ use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; use sctk::compositor::SurfaceData; use sctk::globals::GlobalData; +use sctk::seat::SeatState; use sctk::seat::pointer::{ PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler, }; -use sctk::seat::SeatState; use dpi::{LogicalPosition, PhysicalPosition}; use winit_core::event::{ - ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource, TouchPhase, - WindowEvent, ButtonSource, + ButtonSource, ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource, + TouchPhase, WindowEvent, }; -use crate::state::WinitState; use crate::WindowId; +use crate::state::WinitState; pub mod pointer_gesture; pub mod relative_pointer; diff --git a/winit-wayland/src/seat/pointer/relative_pointer.rs b/winit-wayland/src/seat/pointer/relative_pointer.rs index 8465b720d..c130d5739 100644 --- a/winit-wayland/src/seat/pointer/relative_pointer.rs +++ b/winit-wayland/src/seat/pointer/relative_pointer.rs @@ -3,8 +3,8 @@ use std::ops::Deref; use sctk::reexports::client::globals::{BindError, GlobalList}; -use sctk::reexports::client::{delegate_dispatch, Dispatch}; use sctk::reexports::client::{Connection, QueueHandle}; +use sctk::reexports::client::{Dispatch, delegate_dispatch}; use sctk::reexports::protocols::wp::relative_pointer::zv1::{ client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1, client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1}, @@ -12,8 +12,8 @@ use sctk::reexports::protocols::wp::relative_pointer::zv1::{ use sctk::globals::GlobalData; -use winit_core::event::DeviceEvent; use crate::state::WinitState; +use winit_core::event::DeviceEvent; /// Wrapper around the relative pointer. #[derive(Debug)] diff --git a/winit-wayland/src/state.rs b/winit-wayland/src/state.rs index 577c8c42e..8c06edc2f 100644 --- a/winit-wayland/src/state.rs +++ b/winit-wayland/src/state.rs @@ -4,6 +4,7 @@ use std::sync::{Arc, Mutex}; use foldhash::HashMap; use sctk::compositor::{CompositorHandler, CompositorState}; +use sctk::data_device_manager::DataDeviceManagerState; use sctk::output::{OutputHandler, OutputState}; use sctk::reexports::calloop::LoopHandle; use sctk::reexports::client::backend::ObjectId; @@ -23,6 +24,7 @@ use sctk::subcompositor::SubcompositorState; use winit_core::error::OsError; use crate::WindowId; +use crate::dnd::DndState; use crate::event_loop::sink::EventSink; use crate::output::MonitorHandle; use crate::seat::{ @@ -113,12 +115,18 @@ pub struct WinitState { /// Viewporter state on the given window. pub viewporter_state: Option, + /// Data device manager state on the given window. + pub data_device_manager_state: Option, + /// Fractional scaling manager. pub fractional_scaling_manager: Option, /// Blur manager. pub blur_manager: Option, + /// Drag-and-drop state. + pub dnd_state: DndState, + /// Loop handle to re-register event sources, such as keyboard repeat. pub loop_handle: LoopHandle<'static, Self>, @@ -168,6 +176,17 @@ impl WinitState { (None, None) }; + let data_device_manager_state = match DataDeviceManagerState::bind(globals, queue_handle) { + Ok(state) => Some(state), + Err(e) => { + tracing::warn!( + "Data device manager not available, clipboard and drag-and-drop disabled: \ + {e:?}" + ); + None + }, + }; + let shm = Shm::bind(globals, queue_handle).map_err(|err| os_error!(err))?; let image_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap())); @@ -191,9 +210,12 @@ impl WinitState { window_compositor_updates: Vec::new(), window_events_sink: Default::default(), viewporter_state, + data_device_manager_state, fractional_scaling_manager, blur_manager: BgrEffectManager::new(globals, queue_handle).ok(), + dnd_state: Default::default(), + seats, text_input_state: TextInputState::new(globals, queue_handle).ok(), diff --git a/winit-wayland/src/window/state.rs b/winit-wayland/src/window/state.rs index 7fa98f2a8..0e299b972 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -243,6 +243,12 @@ impl WindowState { } } + // HACK: Currently to get the data device to initiate a drag-and-drop, we iterate through all + // focused seats to find one with a pointer capability. This is definitely wrong. + pub(crate) fn focused_seats(&self) -> impl Iterator { + self.seat_focus.iter() + } + /// Apply closure on the given pointer. fn apply_on_pointer, &WinitPointerData)>( &self, diff --git a/winit-win32/Cargo.toml b/winit-win32/Cargo.toml index 5a5df3ff9..3835afccc 100644 --- a/winit-win32/Cargo.toml +++ b/winit-win32/Cargo.toml @@ -19,6 +19,7 @@ rwh_06.workspace = true serde = { workspace = true, optional = true } smol_str.workspace = true tracing.workspace = true +url.workspace = true winit-core.workspace = true # Platform-specific @@ -32,7 +33,9 @@ windows-sys = { workspace = true, features = [ "Win32_Media", "Win32_System_Com_StructuredStorage", "Win32_System_Com", + "Win32_System_DataExchange", "Win32_System_LibraryLoader", + "Win32_System_Memory", "Win32_System_Ole", "Win32_Security", "Win32_System_SystemInformation", diff --git a/winit-win32/src/definitions.rs b/winit-win32/src/definitions.rs index c5225cfee..9bf87f4b8 100644 --- a/winit-win32/src/definitions.rs +++ b/winit-win32/src/definitions.rs @@ -3,8 +3,9 @@ use std::ffi::c_void; -use windows_sys::Win32::Foundation::{HWND, POINTL}; +use windows_sys::Win32::Foundation::{HWND, POINT, POINTL}; use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM}; +use windows_sys::Win32::UI::Shell::SHDRAGIMAGE; use windows_sys::core::{BOOL, GUID, HRESULT}; pub type IUnknown = *mut c_void; @@ -37,7 +38,7 @@ pub struct IDataObjectVtbl { pformatetc: *const FORMATETC, pmedium: *mut STGMEDIUM, ) -> HRESULT, - QueryGetData: + pub QueryGetData: unsafe extern "system" fn(This: *mut IDataObject, pformatetc: *const FORMATETC) -> HRESULT, pub GetCanonicalFormatEtc: unsafe extern "system" fn( This: *mut IDataObject, @@ -47,7 +48,7 @@ pub struct IDataObjectVtbl { pub SetData: unsafe extern "system" fn( This: *mut IDataObject, pformatetc: *const FORMATETC, - pformatetcOut: *const FORMATETC, + pmedium: *const STGMEDIUM, fRelease: BOOL, ) -> HRESULT, pub EnumFormatEtc: unsafe extern "system" fn( @@ -69,6 +70,81 @@ pub struct IDataObjectVtbl { ) -> HRESULT, } +#[repr(C)] +pub struct IEnumFORMATETCVtbl { + pub parent: IUnknownVtbl, + pub Next: unsafe extern "system" fn( + This: *mut IEnumFORMATETC, + celt: u32, + rgelt: *mut FORMATETC, + pceltFetched: *mut u32, + ) -> HRESULT, + pub Skip: unsafe extern "system" fn(This: *mut IEnumFORMATETC, celt: u32) -> HRESULT, + pub Reset: unsafe extern "system" fn(This: *mut IEnumFORMATETC) -> HRESULT, + pub Clone: unsafe extern "system" fn( + This: *mut IEnumFORMATETC, + ppenum: *mut *mut IEnumFORMATETC, + ) -> HRESULT, +} + +pub type IDragSourceHelper = *mut c_void; + +#[repr(C)] +pub struct IDragSourceHelperVtbl { + pub parent: IUnknownVtbl, + pub InitializeFromBitmap: unsafe extern "system" fn( + This: *mut IDragSourceHelper, + pshdi: *const SHDRAGIMAGE, + pDataObject: *mut IDataObject, + ) -> HRESULT, + pub InitializeFromWindow: unsafe extern "system" fn( + This: *mut IDragSourceHelper, + hwnd: HWND, + ppt: *const POINT, + pDataObject: *mut IDataObject, + ) -> HRESULT, +} + +pub type IDropTargetHelper = *mut c_void; + +#[repr(C)] +pub struct IDropTargetHelperVtbl { + pub parent: IUnknownVtbl, + pub DragEnter: unsafe extern "system" fn( + This: *mut IDropTargetHelper, + hwndTarget: HWND, + pDataObject: *mut IDataObject, + ppt: *const POINT, + dwEffect: u32, + ) -> HRESULT, + pub DragLeave: unsafe extern "system" fn(This: *mut IDropTargetHelper) -> HRESULT, + pub DragOver: unsafe extern "system" fn( + This: *mut IDropTargetHelper, + ppt: *const POINT, + dwEffect: u32, + ) -> HRESULT, + pub Drop: unsafe extern "system" fn( + This: *mut IDropTargetHelper, + pDataObject: *mut IDataObject, + ppt: *const POINT, + dwEffect: u32, + ) -> HRESULT, + pub Show: unsafe extern "system" fn(This: *mut IDropTargetHelper, fShow: BOOL) -> HRESULT, +} + +pub type IDropSource = *mut c_void; + +#[repr(C)] +pub struct IDropSourceVtbl { + pub parent: IUnknownVtbl, + pub QueryContinueDrag: unsafe extern "system" fn( + This: *mut IDropSource, + fEscapePressed: BOOL, + grfKeyState: u32, + ) -> HRESULT, + pub GiveFeedback: unsafe extern "system" fn(This: *mut IDropSource, dwEffect: u32) -> HRESULT, +} + #[repr(C)] pub struct IDropTargetVtbl { pub parent: IUnknownVtbl, @@ -130,6 +206,22 @@ pub struct ITaskbarList2 { pub lpVtbl: *const ITaskbarList2Vtbl, } +/// Defined in `objidl.h`. +pub const IID_IDataObject: GUID = GUID::from_u128(0x0000010e_0000_0000_c000_000000000046); + +/// Defined in `oleidl.h`. +pub const IID_IDropSource: GUID = GUID::from_u128(0x00000121_0000_0000_c000_000000000046); + +/// Defined in `objidl.h`. +pub const IID_IEnumFORMATETC: GUID = GUID::from_u128(0x00000103_0000_0000_c000_000000000046); + +/// Defined in `shobjidl_core.h`. +pub const IID_IDragSourceHelper: GUID = GUID::from_u128(0xde5bf786_477a_11d2_839d_00c04fd918d0); + +/// Defined in `shobjidl_core.h`. +pub const IID_IDropTargetHelper: GUID = GUID::from_u128(0x4657278b_411b_11d2_839a_00c04fd918d0); + +/// Defined in `shobjidl_core.h`. pub const CLSID_TaskbarList: GUID = GUID { data1: 0x56fdf344, data2: 0xfd6d, @@ -137,6 +229,7 @@ pub const CLSID_TaskbarList: GUID = GUID { data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], }; +/// Defined in `shobjidl_core.h`. pub const IID_ITaskbarList: GUID = GUID { data1: 0x56fdf342, data2: 0xfd6d, @@ -144,6 +237,7 @@ pub const IID_ITaskbarList: GUID = GUID { data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90], }; +/// Defined in `shobjidl_core.h`. pub const IID_ITaskbarList2: GUID = GUID { data1: 0x602d4995, data2: 0xb13a, diff --git a/winit-win32/src/dnd.rs b/winit-win32/src/dnd.rs new file mode 100644 index 000000000..7401e61b4 --- /dev/null +++ b/winit-win32/src/dnd.rs @@ -0,0 +1,1615 @@ +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::ffi::{OsStr, OsString, c_void}; +use std::io; +use std::num::NonZeroU32; +use std::ops::{BitOr, ControlFlow}; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::path::PathBuf; +use std::ptr::NonNull; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{self, AtomicI64, AtomicUsize, Ordering}; + +use dpi::PhysicalPosition; +use windows_sys::Win32::Foundation::{ + DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, DRAGDROP_S_USEDEFAULTCURSORS, DV_E_FORMATETC, E_ABORT, + E_FAIL, E_NOINTERFACE, E_NOTIMPL, E_UNEXPECTED, GlobalFree, HGLOBAL, HWND, + OLE_E_ADVISENOTSUPPORTED, POINT, POINTL, S_FALSE, S_OK, +}; +use windows_sys::Win32::Graphics::Gdi::ScreenToClient; +use windows_sys::Win32::System::Com::{ + DVASPECT_CONTENT, FORMATETC, STGMEDIUM, TYMED_ENHMF, TYMED_FILE, TYMED_GDI, TYMED_HGLOBAL, + TYMED_MFPICT, +}; +use windows_sys::Win32::System::DataExchange::RegisterClipboardFormatW; +use windows_sys::Win32::System::Memory::{ + GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock, +}; +use windows_sys::Win32::System::Ole::{ + CF_HDROP, CF_UNICODETEXT, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, + OleDuplicateData, ReleaseStgMedium, +}; +use windows_sys::Win32::System::SystemServices::{ + MK_LBUTTON, MK_MBUTTON, MK_RBUTTON, MK_XBUTTON1, MK_XBUTTON2, +}; +use windows_sys::Win32::UI::Shell::{DROPFILES, DragQueryFileW, HDROP}; +use windows_sys::core::{BOOL, GUID, HRESULT, IID_IUnknown}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData, +}; +use winit_core::event::WindowEvent; +use winit_core::event_loop::DndAction; +use winit_core::window::WindowId; + +use crate::definitions::{ + IDataObject, IDataObjectVtbl, IDropSource, IDropSourceVtbl, IDropTarget, IDropTargetHelper, + IDropTargetHelperVtbl, IDropTargetVtbl, IEnumFORMATETC, IEnumFORMATETCVtbl, IID_IDataObject, + IID_IDropSource, IID_IDropTargetHelper, IID_IEnumFORMATETC, IUnknown, IUnknownVtbl, +}; +use crate::event_loop::EventLoopRunner; +use crate::util; + +// TODO: Exposing the full native API to client applications is too error-prone so long as +// winit is still manually implementing refcounting and using the win32 APIs. For now, we +// just eagerly read all the data supported by cross-platform type hints on Windows. This +// would be resolved by migrating to `windows-rs`. +#[derive(Debug)] +pub(crate) struct DataObject { + data: HashMap, +} + +impl DataObject { + unsafe fn from_idataobject(data_obj: *const IDataObject) -> Self { + let mut data = HashMap::new(); + + if let Some(text) = unsafe { read_unicode_text(data_obj) } { + data.insert(TypeHint::Plaintext, SendData::String(text)); + } + + if let Some(uris) = unsafe { read_uri_list(data_obj) } { + if !uris.is_empty() { + data.insert(TypeHint::UriList, SendData::Uris(uris)); + } + } + + if let Some(png) = unsafe { read_png(data_obj) } { + if !png.is_empty() { + data.insert(TypeHint::Image { extension_hint: Some("png") }, SendData::Bytes(png)); + } + } + + Self { data } + } + + fn resolve(&self, requested: TypeHint) -> Option { + self.data.keys().copied().find(|stored| stored.matches(&requested)) + } +} + +/// RAII wrapper around an STGMEDIUM returned by IDataObject::GetData, releasing it on drop. +struct StgMedium(STGMEDIUM); + +impl StgMedium { + /// Returns `None` if the object doesn't provide the format. + unsafe fn get(data_obj: *const IDataObject, cf_format: u16) -> Option { + let format = FORMATETC { + cfFormat: cf_format, + ptd: std::ptr::null_mut(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL as u32, + }; + + let mut medium = unsafe { std::mem::zeroed::() }; + let get_data = unsafe { (*(*data_obj).cast::()).GetData }; + if unsafe { get_data(data_obj as *mut _, &format, &mut medium) } < 0 { + return None; + } + + Some(Self(medium)) + } + + fn hglobal(&self) -> HGLOBAL { + unsafe { self.0.u.hGlobal } + } +} + +impl Drop for StgMedium { + fn drop(&mut self) { + unsafe { ReleaseStgMedium(&mut self.0) }; + } +} + +unsafe fn read_unicode_text(data_obj: *const IDataObject) -> Option { + let medium = unsafe { StgMedium::get(data_obj, CF_UNICODETEXT) }?; + let hglobal = medium.hglobal(); + + let ptr = unsafe { GlobalLock(hglobal) }; + if ptr.is_null() { + return None; + } + + // `CF_UNICODETEXT` is a NUL-terminated UTF-16 string. Cap the scan by the allocation size in + // case the buffer isn't terminated. + let max_units = unsafe { GlobalSize(hglobal) } / std::mem::size_of::(); + let wide = unsafe { std::slice::from_raw_parts(ptr.cast::(), max_units) }; + let len = wide.iter().position(|&c| c == 0).unwrap_or(max_units); + let text = String::from_utf16_lossy(&wide[..len]); + + unsafe { GlobalUnlock(hglobal) }; + + Some(text) +} + +unsafe fn read_uri_list(data_obj: *const IDataObject) -> Option> { + let medium = unsafe { StgMedium::get(data_obj, CF_HDROP) }?; + let hdrop = medium.hglobal() as HDROP; + + // The second parameter (0xFFFFFFFF) instructs the function to return the item count. + let item_count = unsafe { DragQueryFileW(hdrop, 0xffff_ffff, std::ptr::null_mut(), 0) }; + + let mut paths = Vec::with_capacity(item_count as usize); + for i in 0..item_count { + // Query the path length (excluding the NUL terminator), reserve room for it plus the + // terminator, then copy. `set_len` uses the count actually written, so a short copy can + // never expose uninitialized memory. + let character_count = unsafe { DragQueryFileW(hdrop, i, std::ptr::null_mut(), 0) } as usize; + + let mut path_buf = Vec::::with_capacity(character_count + 1); + let copied = + unsafe { DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), character_count as u32 + 1) } + as usize; + unsafe { path_buf.set_len(copied) }; + + let path = PathBuf::from(OsString::from_wide(&path_buf)); + + paths.push( + url::Url::from_file_path(&path) + .map(String::from) + .unwrap_or_else(|_| path.to_string_lossy().into_owned()), + ); + } + + Some(paths) +} + +unsafe fn read_png(data_obj: *const IDataObject) -> Option> { + let format_name = util::encode_wide("PNG"); + let format = unsafe { RegisterClipboardFormatW(format_name.as_ptr()) }; + if format == 0 { + return None; + } + + let medium = unsafe { StgMedium::get(data_obj, format as u16) }?; + let hglobal = medium.hglobal(); + + let ptr = unsafe { GlobalLock(hglobal) }; + if ptr.is_null() { + return None; + } + + let len = unsafe { GlobalSize(hglobal) }; + let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::(), len) }.to_vec(); + + unsafe { GlobalUnlock(hglobal) }; + + Some(bytes) +} + +#[derive(Debug)] +pub(crate) struct WinDataTransfer { + data: Arc, +} + +impl WinDataTransfer { + pub(crate) fn new(data: Arc) -> Self { + Self { data } + } +} + +impl DataTransfer for WinDataTransfer { + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>, + ) { + for hint in self.data.data.keys() { + if let ControlFlow::Break(()) = func(hint) { + break; + } + } + } +} + +#[derive(Debug)] +pub(crate) struct WinTypedData { + type_: TypeHint, + data: Arc, +} + +impl WinTypedData { + pub(crate) fn new(data: Arc, requested: TypeHint) -> Option { + let type_ = data.resolve(requested)?; + Some(Self { type_, data }) + } +} + +impl TypedData for WinTypedData { + fn type_(&self) -> &dyn TransferType { + &self.type_ + } + + fn try_read(&self) -> Option> { + match self.data.data.get(&self.type_)? { + SendData::Bytes(bytes) => Some(Box::new(io::Cursor::new(bytes.clone()))), + SendData::String(string) => { + Some(Box::new(io::Cursor::new(string.clone().into_bytes()))) + }, + // Windows URI drag-and-drop can't be neatly expressed as a binary blob. + SendData::Uris(_) => None, + } + } + + fn try_as_uris(&self) -> io::Result> { + match self.data.data.get(&self.type_) { + Some(SendData::Uris(uris)) => Ok(uris.clone()), + _ => Err(io::ErrorKind::InvalidData.into()), + } + } + + fn try_as_string(&self) -> io::Result { + match self.data.data.get(&self.type_) { + Some(SendData::String(string)) => Ok(string.clone()), + _ => Err(io::ErrorKind::InvalidData.into()), + } + } +} + +#[repr(C)] +pub struct FileDropHandlerData { + interface: IDropTarget, + refcount: AtomicUsize, + window: HWND, + runner: Rc, + send_event: Box, + active_data_transfer_id: Option, + // Shell drop-target helper. Lazy-init on first DragEnter; `None` means "not yet created" or + // "creation failed and we're running without a drag image". Forwarding to this is what + // makes the source's `IDragSourceHelper` bitmap actually render under the cursor over our + // own window and any other helper-aware target. + drop_target_helper: Option>, +} + +pub struct FileDropHandler { + data: *mut FileDropHandlerData, +} + +#[allow(non_snake_case)] +impl FileDropHandler { + pub(crate) fn new( + window: HWND, + runner: Rc, + send_event: Box, + ) -> FileDropHandler { + let data = Box::new(FileDropHandlerData { + interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl }, + refcount: AtomicUsize::new(1), + window, + runner, + send_event, + active_data_transfer_id: None, + drop_target_helper: None, + }); + FileDropHandler { data: Box::into_raw(data) } + } + + /// Lazy-create the shell drop-target helper. Returns `None` if creation failed; callers + /// should treat that as "no drag image" and continue silently - failure is purely cosmetic. + unsafe fn ensure_drop_target_helper( + data: &mut FileDropHandlerData, + ) -> Option> { + use windows_sys::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance}; + use windows_sys::Win32::UI::Shell::CLSID_DragDropHelper; + + if let Some(helper) = data.drop_target_helper { + return Some(helper); + } + let mut helper: *mut IDropTargetHelper = std::ptr::null_mut(); + let hr = unsafe { + CoCreateInstance( + &CLSID_DragDropHelper, + std::ptr::null_mut(), + CLSCTX_ALL, + &IID_IDropTargetHelper, + &mut helper as *mut _ as *mut _, + ) + }; + if hr < 0 { + return None; + } + let helper = NonNull::new(helper)?; + data.drop_target_helper = Some(helper); + Some(helper) + } + + unsafe fn helper_vtbl(helper: NonNull) -> &'static IDropTargetHelperVtbl { + unsafe { &*(*(helper.as_ptr() as *mut *const IDropTargetHelperVtbl)) } + } + + pub(crate) unsafe fn interface_unchecked_mut(&mut self) -> &mut IDropTarget { + unsafe { &mut (*self.data).interface } + } + + // Implement IUnknown + unsafe extern "system" fn QueryInterface( + _this: *mut IUnknown, + _riid: *const GUID, + _ppvObject: *mut *mut c_void, + ) -> HRESULT { + // This function doesn't appear to be required for an `IDropTarget`. + // An implementation would be nice however. + // Can't use `unimplemented` here as it's invalid to panic over an FFI boundary. + tracing::warn!("`QueryInterface` called, but it was unimplemented"); + E_FAIL + } + + unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let drop_handler_data = unsafe { Self::from_interface(this) }; + let count = drop_handler_data.refcount.fetch_add(1, Ordering::Relaxed) + 1; + count as u32 + } + + unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let drop_handler = unsafe { Self::from_interface(this) }; + // Release on decrement publishes any writes made through this reference before the + // count is observed by other threads. When we hit zero, fence with Acquire so the + // destructor sees all writes from prior Releases on other threads - the standard + // Arc pattern. Without the fence, dropping the box could race with reads done by + // the last releasing thread on another core. + let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + atomic::fence(Ordering::Acquire); + // Drop any transfer still in flight (e.g. the window was destroyed mid-drag, so no + // `DragLeave`/`Drop` ever arrived to clean it up). + if let Some(id) = drop_handler.active_data_transfer_id.take() { + drop_handler.runner.remove_data_transfer(id); + } + // Release the shell drop-target helper if we created one. + if let Some(helper) = drop_handler.drop_target_helper { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { + (vtbl.parent.Release)(helper.as_ptr() as *mut IUnknown); + } + } + // Destroy the underlying data + drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) }); + } + count as u32 + } + + unsafe extern "system" fn DragEnter( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: u32, + pt: POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + let drop_handler = unsafe { Self::from_interface(this) }; + // If this is a self-drop (we initiated the drag from this process), reuse the source's id + // and seed actions from the mask declared at `start_drag` - the app's `DragEntered` + // handler can't call `set_valid_actions` in time because it's buffered until `DoDragDrop` + // returns. + let data_transfer_id = drop_handler + .runner + .source_drag + .get() + .map_or_else(next_data_transfer_id, |info| info.id); + drop_handler.active_data_transfer_id = Some(data_transfer_id); + + let wid = WindowId::from_raw(drop_handler.window.addr()); + + let data = Arc::new(unsafe { DataObject::from_idataobject(pDataObj) }); + drop_handler.runner.register_data_transfer(data_transfer_id, wid, data); + + let pt_screen = POINT { x: pt.x, y: pt.y }; + let mut pt_client = pt_screen; + unsafe { + ScreenToClient(drop_handler.window, &mut pt_client); + } + let position = PhysicalPosition::new(pt_client.x as f64, pt_client.y as f64); + (drop_handler.send_event)(WindowEvent::DragEntered { + id: data_transfer_id, + position: Some(position), + }); + + // Get actions after the event handler has run, so that we update it based on the user's + // supplied info. + { + let actions = drop_handler.runner.current_drag_actions(data_transfer_id); + let source_allowed = unsafe { pdwEffect.read() }; + + let new_effect = pick_effect(&actions, grfKeyState, source_allowed); + unsafe { + pdwEffect.write(new_effect); + } + } + + // Forward to the shell drop-target helper so any drag image attached by the source's + // IDragSourceHelper renders the bitmap under the cursor while it's over our window. + if let Some(helper) = unsafe { Self::ensure_drop_target_helper(drop_handler) } { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { + (vtbl.DragEnter)( + helper.as_ptr(), + drop_handler.window, + pDataObj as *mut IDataObject, + &pt_screen, + *pdwEffect, + ); + } + } + + S_OK + } + + unsafe extern "system" fn DragOver( + this: *mut IDropTarget, + grfKeyState: u32, + pt: POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + let drop_handler = unsafe { Self::from_interface(this) }; + let Some(data_transfer_id) = drop_handler.active_data_transfer_id else { + unsafe { + pdwEffect.write(DROPEFFECT_NONE); + } + + return E_ABORT; + }; + + let effects = unsafe { pdwEffect.read() }; + + let pt_screen = POINT { x: pt.x, y: pt.y }; + let mut pt_client = pt_screen; + unsafe { + ScreenToClient(drop_handler.window, &mut pt_client); + } + let position = PhysicalPosition::new(pt_client.x as f64, pt_client.y as f64); + + let proposed_action = drop_handler.runner.proposed_dnd_action(data_transfer_id, effects); + + (drop_handler.send_event)(WindowEvent::DragPosition { + id: data_transfer_id, + position, + proposed_action, + }); + + // Get actions after the event handler has run, so that we update it based on the user's + // supplied info. + let actions = drop_handler.runner.current_drag_actions(data_transfer_id); + let new_effect = pick_effect(&actions, grfKeyState, effects); + unsafe { + pdwEffect.write(new_effect); + } + + if let Some(helper) = drop_handler.drop_target_helper { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { (vtbl.DragOver)(helper.as_ptr(), &pt_screen, new_effect) }; + } + + S_OK + } + + unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { + let drop_handler = unsafe { Self::from_interface(this) }; + let Some(data_transfer_id) = drop_handler.active_data_transfer_id.take() else { + return E_ABORT; + }; + + (drop_handler.send_event)(WindowEvent::DragLeft { id: data_transfer_id }); + drop_handler.runner.remove_data_transfer(data_transfer_id); + + if let Some(helper) = drop_handler.drop_target_helper { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { (vtbl.DragLeave)(helper.as_ptr()) }; + } + + S_OK + } + + unsafe extern "system" fn Drop( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: u32, + pt: POINTL, + pdwEffect: *mut u32, + ) -> HRESULT { + let drop_handler = unsafe { Self::from_interface(this) }; + let Some(data_transfer_id) = drop_handler.active_data_transfer_id.take() else { + unsafe { + *pdwEffect = DROPEFFECT_NONE; + } + + return E_ABORT; + }; + + let effects = unsafe { pdwEffect.read() }; + let proposed_action = drop_handler.runner.proposed_dnd_action(data_transfer_id, effects); + + let pt_screen = POINT { x: pt.x, y: pt.y }; + let mut pt_client = pt_screen; + unsafe { + ScreenToClient(drop_handler.window, &mut pt_client); + } + let pt = pt_client; + let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); + + (drop_handler.send_event)(WindowEvent::DragPosition { + id: data_transfer_id, + position, + proposed_action, + }); + + // New scope to make sure that the `Ref` returned by `current_drag_actions` is dropped + // before we call `remove_data_transfer`. + { + // Get actions after the event handler has run, so that we update it based on the user's + // supplied info. + let actions = drop_handler.runner.current_drag_actions(data_transfer_id); + let source_allowed = unsafe { pdwEffect.read() }; + let proposed_action = + drop_handler.runner.proposed_dnd_action(data_transfer_id, effects); + + // Negotiate the effect first so we can pick the right outgoing event. If the app + // rejected the drop (e.g. via `set_valid_actions(none())`), `pick_effect` returns + // `DROPEFFECT_NONE`; in that case OLE reports back `effect_out == DROPEFFECT_NONE` to + // the source, so the matching observer-facing event is `DragLeft`, not `DragDropped` - + // otherwise target and source see contradictory outcomes. + let effect = pick_effect(&actions, grfKeyState, source_allowed); + let event = if effect == DROPEFFECT_NONE { + WindowEvent::DragLeft { id: data_transfer_id } + } else { + WindowEvent::DragDropped { id: data_transfer_id, proposed_action } + }; + (drop_handler.send_event)(event); + unsafe { + *pdwEffect = effect; + } + + if let Some(helper) = drop_handler.drop_target_helper { + let vtbl = unsafe { Self::helper_vtbl(helper) }; + unsafe { + (vtbl.Drop)(helper.as_ptr(), pDataObj as *mut IDataObject, &pt_screen, effect); + } + } + } + + // External drop: the app's handler dispatched synchronously above and has already read + // the data; safe to release the cache. Self-drop: the handler is buffered and hasn't + // run yet, so defer cleanup until after `dispatch_buffered_events` drains. + if drop_handler.runner.source_drag.get().is_some() { + drop_handler.runner.defer_source_drag_cleanup(data_transfer_id); + } else { + drop_handler.runner.remove_data_transfer(data_transfer_id); + } + + S_OK + } + + unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData { + unsafe { &mut *(this as *mut _) } + } +} + +impl Drop for FileDropHandler { + fn drop(&mut self) { + unsafe { + FileDropHandler::Release(self.data as *mut IUnknown); + } + } +} + +static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { + parent: IUnknownVtbl { + QueryInterface: FileDropHandler::QueryInterface, + AddRef: FileDropHandler::AddRef, + Release: FileDropHandler::Release, + }, + DragEnter: FileDropHandler::DragEnter, + DragOver: FileDropHandler::DragOver, + DragLeave: FileDropHandler::DragLeave, + Drop: FileDropHandler::Drop, +}; + +pub(crate) type DropEffect = u32; + +/// Map the app's [`DndActions`] to the win32 `DROPEFFECT_*` bitmask. +pub(crate) fn dnd_action_to_dropeffect_mask(action: DndAction) -> DropEffect { + match action { + DndAction::Move => DROPEFFECT_MOVE, + DndAction::Copy => DROPEFFECT_COPY, + DndAction::Link => DROPEFFECT_LINK, + _ => DROPEFFECT_NONE, + } +} + +/// Map the app's [`DndActions`] to the win32 `DROPEFFECT_*` bitmask. +pub(crate) fn dnd_actions_to_dropeffect_mask(actions: &[DndAction]) -> DropEffect { + actions + .iter() + .copied() + .map(dnd_action_to_dropeffect_mask) + .fold(DropEffect::default(), BitOr::bitor) +} + +pub(crate) fn drop_effect_to_dnd_action(effect: DropEffect) -> Option { + match effect { + DROPEFFECT_MOVE => Some(DndAction::Move), + DROPEFFECT_COPY => Some(DndAction::Copy), + DROPEFFECT_LINK => Some(DndAction::Link), + _ => None, + } +} + +// Intersect the app's valid actions with the source's allowed effects, honoring Ctrl/Shift. +fn pick_effect(actions: &[DndAction], key_state: u32, source_allowed: u32) -> u32 { + const MK_SHIFT: u32 = 0x0004; + const MK_CONTROL: u32 = 0x0008; + + // TODO: On macOS, option will disable copy (so if copy has higher precedence than move you can + // select move via option). Is alt expected to act similarly on Windows? + let mut allowed = dnd_actions_to_dropeffect_mask(actions) & source_allowed; + if allowed == 0 { + return DROPEFFECT_NONE; + } + + // If holding a modifier would result in no valid values, ignore + // Need to use filter instead of if-let chains for 1.85 compatibility. + if let Some(new_allowed) = + NonZeroU32::new(allowed & !DROPEFFECT_MOVE).filter(|_| key_state & MK_CONTROL != 0) + { + allowed = new_allowed.get(); + } + + // If holding a modifier would result in no valid values, ignore + // Need to use filter instead of if-let chains for 1.85 compatibility. + if let Some(new_allowed) = + NonZeroU32::new(allowed & !DROPEFFECT_COPY).filter(|_| key_state & MK_SHIFT != 0) + { + allowed = new_allowed.get(); + } + + for action in actions { + let effect = dnd_action_to_dropeffect_mask(*action); + if (allowed & effect) != 0 { + return effect; + } + } + + DROPEFFECT_NONE +} + +// ============================================================================ +// Source side: providing data and controlling a `DoDragDrop` session. +// ============================================================================ + +/// Mint a unique [`DataTransferId`] for either a target-side `DragEnter` or a source-side +/// `start_drag` so the two never collide. +pub(crate) fn next_data_transfer_id() -> DataTransferId { + static COUNTER: AtomicI64 = AtomicI64::new(0); + DataTransferId::from_raw(COUNTER.fetch_add(1, Ordering::Relaxed)) +} + +fn guids_eq(a: &GUID, b: &GUID) -> bool { + a.data1 == b.data1 && a.data2 == b.data2 && a.data3 == b.data3 && a.data4 == b.data4 +} + +fn register_clipboard_format(name: &str) -> Option { + let wide = util::encode_wide(name); + let atom = unsafe { RegisterClipboardFormatW(wide.as_ptr()) }; + (atom != 0).then_some(atom as u16) +} + +// Returns the (cf_format, specialised hint) pairs we can lower `hint` to. +// +// Most hints map 1:1. Wildcard hints like `Image { extension_hint: None }` - +// the cross-platform way to say "any image format" - fan out to one entry per +// concrete format we support, each paired with the specialised hint so +// `data_for_type` is invoked with a concrete extension instead of `None`. +fn cf_formats_for_hint(hint: TypeHint) -> Vec<(u16, TypeHint)> { + fn one(cf: Option, hint: TypeHint) -> Vec<(u16, TypeHint)> { + cf.map(|cf| vec![(cf, hint)]).unwrap_or_default() + } + match hint { + TypeHint::Plaintext => vec![(CF_UNICODETEXT, hint)], + TypeHint::UriList => vec![(CF_HDROP, hint)], + TypeHint::Html => one(register_clipboard_format("HTML Format"), hint), + TypeHint::Image { extension_hint: Some("png") } => { + one(register_clipboard_format("PNG"), hint) + }, + TypeHint::Image { extension_hint: None } => { + // Fan out to every concrete image format we can produce. + let mut out = Vec::new(); + if let Some(cf) = register_clipboard_format("PNG") { + out.push((cf, TypeHint::Image { extension_hint: Some("png") })); + } + out + }, + _ => Vec::new(), + } +} + +/// Duplicate a `STGMEDIUM` for handing out via `GetData` without losing the original. +/// +/// Delegates to `OleDuplicateData` which knows how to clone HGLOBAL, HBITMAP, HENHMETAFILE, +/// HMETAFILEPICT and file-name mediums. Returns `None` for tymeds the shell helper doesn't use +/// (interface-based mediums like IStream / IStorage) so we never hand out an aliased pointer +/// the caller would later `Release` once we also drop ours. +unsafe fn duplicate_stgmedium(src: &STGMEDIUM, cf_format: u16) -> Option { + use windows_sys::Win32::Foundation::HANDLE; + + // Pull the handle out of the union by tymed. The shell drag helper only uses HGLOBAL today, + // but forwarding every handle-typed tymed `OleDuplicateData` understands is essentially + // free, so do it. Interface-based tymeds (IStream / IStorage) deliberately fall through - + // duplicating them properly requires AddRef, not OleDuplicateData. + let tymed = src.tymed as i32; + let handle: HANDLE = unsafe { + if tymed == TYMED_HGLOBAL { + src.u.hGlobal as HANDLE + } else if tymed == TYMED_FILE { + src.u.lpszFileName as HANDLE + } else if tymed == TYMED_GDI { + src.u.hBitmap as HANDLE + } else if tymed == TYMED_MFPICT { + src.u.hMetaFilePict as HANDLE + } else if tymed == TYMED_ENHMF { + src.u.hEnhMetaFile as HANDLE + } else { + return None; + } + }; + let dup = unsafe { OleDuplicateData(handle, cf_format, 0) }; + if dup.is_null() { + return None; + } + let mut out: STGMEDIUM = unsafe { std::mem::zeroed() }; + out.tymed = src.tymed; + if tymed == TYMED_HGLOBAL { + out.u.hGlobal = dup as _; + } else if tymed == TYMED_FILE { + out.u.lpszFileName = dup as _; + } else if tymed == TYMED_GDI { + out.u.hBitmap = dup as _; + } else if tymed == TYMED_MFPICT { + out.u.hMetaFilePict = dup as _; + } else if tymed == TYMED_ENHMF { + out.u.hEnhMetaFile = dup as _; + } else { + // Should be unreachable - the same `tymed` matched the first chain above. Return + // `None` rather than panic: this runs across the COM/FFI boundary, where unwinding + // would be UB. + return None; + } + Some(out) +} + +fn alloc_hglobal_from(src: &[u8]) -> Option { + let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, src.len()) }; + if hglobal.is_null() { + return None; + } + let dst = unsafe { GlobalLock(hglobal) }; + if dst.is_null() { + // `GlobalAlloc` succeeded but locking failed - free before bailing so we don't + // leak the moveable handle. + unsafe { GlobalFree(hglobal) }; + return None; + } + unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), dst as *mut u8, src.len()) }; + unsafe { GlobalUnlock(hglobal) }; + Some(hglobal) +} + +/// Build the [HTML Clipboard Format] wire bytes from an app-supplied HTML string. +/// +/// The format is a UTF-8 buffer with a small text header naming byte offsets into itself. Each +/// offset placeholder is exactly 10 zero-padded decimal digits, which fixes the header at a +/// known constant length and removes the chicken-and-egg between header length and offsets. +/// +/// If the input already contains `` we trust the caller's wrapping; +/// otherwise we wrap it in a minimal `` document with fragment markers. +/// +/// [HTML Clipboard Format]: https://learn.microsoft.com/en-us/windows/win32/dataxchg/html-clipboard-format +fn build_html_clipboard_format(html: &str) -> Vec { + use std::borrow::Cow; + + const START_MARKER: &str = ""; + const END_MARKER: &str = ""; + + let body: Cow<'_, str> = if html.contains(START_MARKER) { + Cow::Borrowed(html) + } else { + Cow::Owned(format!("\r\n{START_MARKER}{html}{END_MARKER}\r\n")) + }; + + const HEADER_LEN: usize = concat!( + "Version:0.9\r\n", + "StartHTML:0000000000\r\n", + "EndHTML:0000000000\r\n", + "StartFragment:0000000000\r\n", + "EndFragment:0000000000\r\n", + ) + .len(); + + let start_html = HEADER_LEN; + let end_html = HEADER_LEN + body.len(); + let start_fragment = HEADER_LEN + body.find(START_MARKER).unwrap() + START_MARKER.len(); + let end_fragment = HEADER_LEN + body.find(END_MARKER).unwrap(); + + let header = format!( + "Version:0.9\r\nStartHTML:{start_html:010}\r\nEndHTML:{end_html:010}\r\nStartFragment:\ + {start_fragment:010}\r\nEndFragment:{end_fragment:010}\r\n", + ); + debug_assert_eq!(header.len(), HEADER_LEN); + + let mut buf = Vec::with_capacity(header.len() + body.len()); + buf.extend_from_slice(header.as_bytes()); + buf.extend_from_slice(body.as_bytes()); + buf +} + +/// Convert app-supplied [`SendData`] into an `HGLOBAL`-backed [`STGMEDIUM`]. +/// +/// Callers must have already validated that the `SendData` variant matches the on-the-wire shape +/// the requested clipboard format expects (see `variant_matches_hint` at the `GetData` call site). +unsafe fn send_data_to_stgmedium(data: SendData, hint: TypeHint) -> Option { + let hglobal = match data { + SendData::String(s) if matches!(hint, TypeHint::Html) => { + // HTML Clipboard Format: UTF-8 with a Version/StartHTML/EndHTML/StartFragment/ + // EndFragment header. Targets parse the header before reading the wrapped HTML. + let bytes = build_html_clipboard_format(&s); + alloc_hglobal_from(&bytes)? + }, + SendData::String(s) => { + // UTF-16 + NUL - used for `CF_UNICODETEXT` and other text-ish registered formats. + let utf16 = util::encode_wide(&s); + let utf16_bytes = + unsafe { std::slice::from_raw_parts(utf16.as_ptr() as *const u8, utf16.len() * 2) }; + alloc_hglobal_from(utf16_bytes)? + }, + SendData::Uris(paths) => { + // CF_HDROP: `DROPFILES` header + double-NUL-terminated UTF-16 path list. + let mut wide: Vec = Vec::new(); + for path in paths { + let path_from_uri_owned; + let encoded = if let Some(path_from_uri) = + url::Url::parse(&path).ok().and_then(|url| url.to_file_path().ok()) + { + path_from_uri_owned = path_from_uri; + OsStr::new(&path_from_uri_owned).encode_wide() + } else { + OsStr::new(&path).encode_wide() + }; + + wide.extend(encoded); + wide.push(0); + } + wide.push(0); + let header = DROPFILES { + pFiles: std::mem::size_of::() as u32, + pt: POINT { x: 0, y: 0 }, + fNC: 0, + fWide: 1, + }; + let total = std::mem::size_of::() + wide.len() * 2; + let hglobal = unsafe { GlobalAlloc(GMEM_MOVEABLE, total) }; + if hglobal.is_null() { + return None; + } + let dst = unsafe { GlobalLock(hglobal) }; + if dst.is_null() { + unsafe { GlobalFree(hglobal) }; + return None; + } + unsafe { + std::ptr::write_unaligned(dst as *mut DROPFILES, header); + let paths_dst = (dst as *mut u8).add(std::mem::size_of::()); + std::ptr::copy_nonoverlapping( + wide.as_ptr() as *const u8, + paths_dst, + wide.len() * 2, + ); + GlobalUnlock(hglobal); + } + hglobal + }, + SendData::Bytes(b) => alloc_hglobal_from(&b)?, + }; + + let mut medium = unsafe { std::mem::zeroed::() }; + medium.tymed = TYMED_HGLOBAL as u32; + medium.u.hGlobal = hglobal; + Some(medium) +} + +/// True if the `SendData` variant carries the on-the-wire shape OLE expects for `hint`'s mapped +/// clipboard format. Mismatches (e.g. `SendData::String` for a `UriList` hint) would otherwise +/// produce malformed `HGLOBAL` payloads (e.g. UTF-16 text mislabeled as `CF_HDROP`), which the +/// target would parse as wild offsets - memory corruption in the receiving process. +fn variant_matches_hint(hint: TypeHint, data: &SendData) -> bool { + matches!( + (hint, data), + (TypeHint::UriList, SendData::Uris(_)) + | (TypeHint::Plaintext | TypeHint::Html | TypeHint::Rtf, SendData::String(_)) + | (TypeHint::Image { .. } | TypeHint::Audio { .. }, SendData::Bytes(_)) + ) +} + +// ---- IEnumFORMATETC: a cursor over a precomputed `Vec`. ----------- + +#[repr(C)] +#[allow(non_snake_case)] +struct IEnumFORMATETCInterface { + lpVtbl: *const IEnumFORMATETCVtbl, +} + +/// Generates the shared `IUnknown` boilerplate for our hand-rolled COM source-side objects. +/// +/// Each such object is a `Box`-allocated `#[repr(C)]` struct whose first field is its COM +/// interface and which carries an `AtomicUsize` `refcount`. The thunks are identical across +/// objects apart from the concrete type and the one extra IID accepted by `QueryInterface`, so +/// the subtle refcount memory ordering lives in exactly one place. +macro_rules! com_iunknown_impl { + ($ty:ty, $extra_iid:expr) => { + #[allow(non_snake_case)] + impl $ty { + unsafe fn from_interface<'a, I>(this: *mut I) -> &'a mut Self { + unsafe { &mut *(this as *mut _) } + } + + unsafe extern "system" fn QueryInterface( + this: *mut IUnknown, + riid: *const GUID, + ppv: *mut *mut c_void, + ) -> HRESULT { + let riid = unsafe { &*riid }; + if guids_eq(riid, &IID_IUnknown) || guids_eq(riid, $extra_iid) { + unsafe { *ppv = this as *mut c_void }; + unsafe { Self::AddRef(this) }; + S_OK + } else { + unsafe { *ppv = std::ptr::null_mut() }; + E_NOINTERFACE + } + } + + unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + // Mere refcount bump - Relaxed is enough; the caller already holds a synchronized + // reference to the object. + me.refcount.fetch_add(1, Ordering::Relaxed) as u32 + 1 + } + + unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { + let me = unsafe { Self::from_interface(this) }; + // Release on decrement publishes any writes made through this reference before the + // count is observed by other threads. When we hit zero, fence with Acquire so the + // destructor sees all writes from prior Releases on other threads - the standard + // Arc pattern. Without the fence, dropping the box could race with reads done by + // the last releasing thread on another core. + let count = me.refcount.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + atomic::fence(Ordering::Acquire); + drop(unsafe { Box::from_raw(me as *mut Self) }); + } + count as u32 + } + } + }; +} + +#[repr(C)] +struct SourceFormatEnumerator { + interface: IEnumFORMATETCInterface, + refcount: AtomicUsize, + formats: Vec, + cursor: Cell, +} + +com_iunknown_impl!(SourceFormatEnumerator, &IID_IEnumFORMATETC); + +#[allow(non_snake_case)] +impl SourceFormatEnumerator { + fn new_boxed(formats: Vec) -> *mut Self { + Box::into_raw(Box::new(Self { + interface: IEnumFORMATETCInterface { + lpVtbl: &SOURCE_ENUM_FORMATETC_VTBL as *const IEnumFORMATETCVtbl, + }, + refcount: AtomicUsize::new(1), + formats, + cursor: Cell::new(0), + })) + } + + unsafe extern "system" fn Next( + this: *mut IEnumFORMATETC, + celt: u32, + rgelt: *mut FORMATETC, + pcelt_fetched: *mut u32, + ) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let cursor = me.cursor.get(); + let to_copy = (celt as usize).min(me.formats.len().saturating_sub(cursor)); + for i in 0..to_copy { + unsafe { *rgelt.add(i) = me.formats[cursor + i] }; + } + me.cursor.set(cursor + to_copy); + if !pcelt_fetched.is_null() { + unsafe { *pcelt_fetched = to_copy as u32 }; + } + if to_copy < celt as usize { S_FALSE } else { S_OK } + } + + unsafe extern "system" fn Skip(this: *mut IEnumFORMATETC, celt: u32) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let new_cursor = me.cursor.get().saturating_add(celt as usize); + if new_cursor > me.formats.len() { + me.cursor.set(me.formats.len()); + S_FALSE + } else { + me.cursor.set(new_cursor); + S_OK + } + } + + unsafe extern "system" fn Reset(this: *mut IEnumFORMATETC) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + me.cursor.set(0); + S_OK + } + + unsafe extern "system" fn Clone( + this: *mut IEnumFORMATETC, + ppenum: *mut *mut IEnumFORMATETC, + ) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let cloned = SourceFormatEnumerator::new_boxed(me.formats.clone()); + unsafe { (*cloned).cursor.set(me.cursor.get()) }; + unsafe { *ppenum = cloned as *mut IEnumFORMATETC }; + S_OK + } +} + +static SOURCE_ENUM_FORMATETC_VTBL: IEnumFORMATETCVtbl = IEnumFORMATETCVtbl { + parent: IUnknownVtbl { + QueryInterface: SourceFormatEnumerator::QueryInterface, + AddRef: SourceFormatEnumerator::AddRef, + Release: SourceFormatEnumerator::Release, + }, + Next: SourceFormatEnumerator::Next, + Skip: SourceFormatEnumerator::Skip, + Reset: SourceFormatEnumerator::Reset, + Clone: SourceFormatEnumerator::Clone, +}; + +// ---- IDataObject (source) --------------------------------------------------- + +#[repr(C)] +#[allow(non_snake_case)] +struct IDataObjectInterface { + lpVtbl: *const IDataObjectVtbl, +} + +/// Owning wrapper around a `STGMEDIUM` that releases the underlying handle on drop. +struct OwnedStgMedium(STGMEDIUM); + +impl Drop for OwnedStgMedium { + fn drop(&mut self) { + unsafe { ReleaseStgMedium(&mut self.0) }; + } +} + +#[repr(C)] +struct SourceDataObjectData { + interface: IDataObjectInterface, + refcount: AtomicUsize, + send_data: RefCell>, + // (cf_format, hint) pairs we advertise to the target. + formats: Vec<(u16, TypeHint)>, + // Formats injected via `SetData` - primarily by `IDragSourceHelper::InitializeFromBitmap`, + // which stores the drag image bits (CFSTR_DRAGIMAGEBITS) and related shell formats here so + // the target-side `IDropTargetHelper` can read them back via `GetData`. + extras: RefCell>, +} + +com_iunknown_impl!(SourceDataObjectData, &IID_IDataObject); + +#[allow(non_snake_case)] +impl SourceDataObjectData { + fn new_boxed(send_data: Box) -> *mut Self { + let mut formats: Vec<(u16, TypeHint)> = Vec::new(); + send_data.for_each_available_type(&mut |ty| { + if let Some(hint) = ty.hint() { + for (cf, specialised) in cf_formats_for_hint(hint) { + if !formats.iter().any(|(c, _)| *c == cf) { + formats.push((cf, specialised)); + } + } + } + ControlFlow::Continue(()) + }); + + Box::into_raw(Box::new(Self { + interface: IDataObjectInterface { + lpVtbl: &SOURCE_DATA_OBJECT_VTBL as *const IDataObjectVtbl, + }, + refcount: AtomicUsize::new(1), + send_data: RefCell::new(send_data), + formats, + extras: RefCell::new(Vec::new()), + })) + } + + unsafe extern "system" fn GetData( + this: *mut IDataObject, + pformatetc_in: *const FORMATETC, + pmedium: *mut STGMEDIUM, + ) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let format = unsafe { &*pformatetc_in }; + if (format.tymed & TYMED_HGLOBAL as u32) == 0 { + return DV_E_FORMATETC; + } + // Shell-helper-injected formats live in `extras`; serve those first by duplicating the + // stored HGLOBAL so the caller can `ReleaseStgMedium` independently of our storage. + if let Ok(extras) = me.extras.try_borrow() { + if let Some((stored_fmt, stored)) = + extras.iter().find(|(f, _)| f.cfFormat == format.cfFormat) + { + if (stored_fmt.tymed & format.tymed) != 0 { + if let Some(dup) = unsafe { duplicate_stgmedium(&stored.0, format.cfFormat) } { + unsafe { *pmedium = dup }; + return S_OK; + } + return E_FAIL; + } + } + } + let Some(&(_, hint)) = me.formats.iter().find(|&&(cf, _)| cf == format.cfFormat) else { + return DV_E_FORMATETC; + }; + // `try_borrow_mut` rather than `borrow_mut`: `data_for_type` is the app's callback and + // may itself reach back into this `IDataObject`. A panic across the `extern "system"` + // boundary would be UB; return `E_UNEXPECTED` instead. + let data = { + let Ok(send_data) = me.send_data.try_borrow_mut() else { + return E_UNEXPECTED; + }; + send_data.data_for_type(&hint) + }; + let Some(data) = data else { + return DV_E_FORMATETC; + }; + if !variant_matches_hint(hint, &data) { + return DV_E_FORMATETC; + } + let Some(medium) = (unsafe { send_data_to_stgmedium(data, hint) }) else { + return E_FAIL; + }; + unsafe { *pmedium = medium }; + S_OK + } + + unsafe extern "system" fn GetDataHere( + _this: *mut IDataObject, + _pformatetc: *const FORMATETC, + _pmedium: *mut STGMEDIUM, + ) -> HRESULT { + E_NOTIMPL + } + + unsafe extern "system" fn QueryGetData( + this: *mut IDataObject, + pformatetc: *const FORMATETC, + ) -> HRESULT { + let me = unsafe { Self::from_interface(this) }; + let format = unsafe { &*pformatetc }; + if (format.tymed & TYMED_HGLOBAL as u32) == 0 { + return DV_E_FORMATETC; + } + if me.formats.iter().any(|&(cf, _)| cf == format.cfFormat) { + return S_OK; + } + if let Ok(extras) = me.extras.try_borrow() { + if extras.iter().any(|(f, _)| f.cfFormat == format.cfFormat) { + return S_OK; + } + } + S_FALSE + } + + unsafe extern "system" fn GetCanonicalFormatEtc( + _this: *mut IDataObject, + _pformatetc_in: *const FORMATETC, + _pformatetc_out: *mut FORMATETC, + ) -> HRESULT { + E_NOTIMPL + } + + unsafe extern "system" fn SetData( + this: *mut IDataObject, + pformatetc: *const FORMATETC, + pmedium: *const STGMEDIUM, + f_release: BOOL, + ) -> HRESULT { + // Primary caller is `IDragSourceHelper::InitializeFromBitmap`, which attaches the drag + // image bits and related shell formats to our data object. We don't interpret them - + // just hold them so `GetData` can hand them back to `IDropTargetHelper`. + let me = unsafe { Self::from_interface(this) }; + if pformatetc.is_null() || pmedium.is_null() { + return E_FAIL; + } + let format = unsafe { *pformatetc }; + let medium = if f_release != 0 { + // We take ownership of the passed-in medium as-is. + unsafe { *pmedium } + } else { + // Caller retains ownership; we must duplicate. + let Some(dup) = (unsafe { duplicate_stgmedium(&*pmedium, format.cfFormat) }) else { + return E_FAIL; + }; + dup + }; + let Ok(mut extras) = me.extras.try_borrow_mut() else { + if f_release != 0 { + // We promised to take ownership but can't store - release immediately so we + // don't leak. + let mut m = medium; + unsafe { ReleaseStgMedium(&mut m) }; + } + return E_UNEXPECTED; + }; + // Replace any earlier entry with the same format - last-write-wins matches what real + // shell apps do and avoids growing the vec unboundedly on repeated SetData calls. + if let Some(slot) = extras.iter_mut().find(|(f, _)| f.cfFormat == format.cfFormat) { + slot.0 = format; + slot.1 = OwnedStgMedium(medium); + } else { + extras.push((format, OwnedStgMedium(medium))); + } + S_OK + } + + unsafe extern "system" fn EnumFormatEtc( + this: *mut IDataObject, + dw_direction: u32, + ppenum: *mut *mut IEnumFORMATETC, + ) -> HRESULT { + const DATADIR_GET: u32 = 1; + if dw_direction != DATADIR_GET { + return E_NOTIMPL; + } + let me = unsafe { Self::from_interface(this) }; + let mut formats: Vec = me + .formats + .iter() + .map(|&(cf, _)| FORMATETC { + cfFormat: cf, + ptd: std::ptr::null_mut(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL as u32, + }) + .collect(); + if let Ok(extras) = me.extras.try_borrow() { + for (fmt, _) in extras.iter() { + if !formats.iter().any(|f| f.cfFormat == fmt.cfFormat) { + formats.push(*fmt); + } + } + } + let enumerator = SourceFormatEnumerator::new_boxed(formats); + unsafe { *ppenum = enumerator as *mut IEnumFORMATETC }; + S_OK + } + + unsafe extern "system" fn DAdvise( + _this: *mut IDataObject, + _pformatetc: *const FORMATETC, + _advf: u32, + _adv_sink: *const crate::definitions::IAdviseSink, + _pdw_connection: *mut u32, + ) -> HRESULT { + OLE_E_ADVISENOTSUPPORTED + } + + unsafe extern "system" fn DUnadvise(_this: *mut IDataObject, _connection: u32) -> HRESULT { + OLE_E_ADVISENOTSUPPORTED + } + + unsafe extern "system" fn EnumDAdvise( + _this: *mut IDataObject, + _ppenum_advise: *const *const crate::definitions::IEnumSTATDATA, + ) -> HRESULT { + OLE_E_ADVISENOTSUPPORTED + } +} + +static SOURCE_DATA_OBJECT_VTBL: IDataObjectVtbl = IDataObjectVtbl { + parent: IUnknownVtbl { + QueryInterface: SourceDataObjectData::QueryInterface, + AddRef: SourceDataObjectData::AddRef, + Release: SourceDataObjectData::Release, + }, + GetData: SourceDataObjectData::GetData, + GetDataHere: SourceDataObjectData::GetDataHere, + QueryGetData: SourceDataObjectData::QueryGetData, + GetCanonicalFormatEtc: SourceDataObjectData::GetCanonicalFormatEtc, + SetData: SourceDataObjectData::SetData, + EnumFormatEtc: SourceDataObjectData::EnumFormatEtc, + DAdvise: SourceDataObjectData::DAdvise, + DUnadvise: SourceDataObjectData::DUnadvise, + EnumDAdvise: SourceDataObjectData::EnumDAdvise, +}; + +pub(crate) struct SourceDataObject { + data: *mut SourceDataObjectData, +} + +impl SourceDataObject { + pub(crate) fn new(send_data: Box) -> Self { + Self { data: SourceDataObjectData::new_boxed(send_data) } + } + + pub(crate) fn interface_ptr(&self) -> *mut c_void { + self.data as *mut c_void + } +} + +impl Drop for SourceDataObject { + fn drop(&mut self) { + unsafe { SourceDataObjectData::Release(self.data as *mut IUnknown) }; + } +} + +// ---- IDropSource ------------------------------------------------------------ + +#[repr(C)] +#[allow(non_snake_case)] +struct IDropSourceInterface { + lpVtbl: *const IDropSourceVtbl, +} + +#[repr(C)] +struct DropSourceData { + interface: IDropSourceInterface, + refcount: AtomicUsize, +} + +com_iunknown_impl!(DropSourceData, &IID_IDropSource); + +#[allow(non_snake_case)] +impl DropSourceData { + fn new_boxed() -> *mut Self { + Box::into_raw(Box::new(Self { + interface: IDropSourceInterface { lpVtbl: &DROP_SOURCE_VTBL as *const IDropSourceVtbl }, + refcount: AtomicUsize::new(1), + })) + } + + unsafe extern "system" fn QueryContinueDrag( + _this: *mut IDropSource, + escape_pressed: BOOL, + grf_key_state: u32, + ) -> HRESULT { + // Drop when no mouse button is pressed - matches Microsoft's documented + // `IDropSource` example and covers right-button / middle-button drags as + // well as the common left-button case. Hardcoding `MK_LBUTTON` would make + // a right-drag (e.g. context-menu drag) never terminate by mouse release. + const ANY_MOUSE_BUTTON: u32 = + MK_LBUTTON | MK_RBUTTON | MK_MBUTTON | MK_XBUTTON1 | MK_XBUTTON2; + if escape_pressed != 0 { + return DRAGDROP_S_CANCEL; + } + if (grf_key_state & ANY_MOUSE_BUTTON) == 0 { + return DRAGDROP_S_DROP; + } + S_OK + } + + unsafe extern "system" fn GiveFeedback(_this: *mut IDropSource, _dw_effect: u32) -> HRESULT { + DRAGDROP_S_USEDEFAULTCURSORS + } +} + +static DROP_SOURCE_VTBL: IDropSourceVtbl = IDropSourceVtbl { + parent: IUnknownVtbl { + QueryInterface: DropSourceData::QueryInterface, + AddRef: DropSourceData::AddRef, + Release: DropSourceData::Release, + }, + QueryContinueDrag: DropSourceData::QueryContinueDrag, + GiveFeedback: DropSourceData::GiveFeedback, +}; + +pub(crate) struct DropSource { + data: *mut DropSourceData, +} + +impl DropSource { + pub(crate) fn new() -> Self { + Self { data: DropSourceData::new_boxed() } + } + + pub(crate) fn interface_ptr(&self) -> *mut c_void { + self.data as *mut c_void + } +} + +impl Drop for DropSource { + fn drop(&mut self) { + unsafe { DropSourceData::Release(self.data as *mut IUnknown) }; + } +} + +/// Attach a drag image to `data_object` so the shell renders the app's icon under the cursor +/// during the drag instead of the default no-image cursor. +/// +/// `rgba` is the icon's pixel buffer in straight RGBA8 with `width * height * 4` bytes; +/// `offset` matches `DragIcon::offset` - `(0, 0)` sits the cursor at the icon's top-left, +/// `(-w/2, -h/2)` centres the icon on the cursor. Windows expresses this as the offset from +/// the icon's upper-left to the cursor hot spot (sign inverted), so we negate when filling +/// in `SHDRAGIMAGE::ptOffset`. +/// +/// Returns `Ok(())` on success. On failure the caller's data object is left untouched and the +/// drag still runs - just without a custom image. We never propagate the error: a missing drag +/// preview is purely cosmetic and shouldn't fail the whole `start_drag`. +pub(crate) unsafe fn apply_drag_image( + data_object: *mut IDataObject, + width: u32, + height: u32, + rgba: &[u8], + offset_x: i32, + offset_y: i32, +) -> Result<(), HRESULT> { + use windows_sys::Win32::Graphics::Gdi::{ + BI_RGB, BITMAPINFO, BITMAPINFOHEADER, CreateDIBSection, DIB_RGB_COLORS, DeleteObject, HDC, + }; + use windows_sys::Win32::System::Com::{CLSCTX_ALL, CoCreateInstance}; + use windows_sys::Win32::UI::Shell::{CLSID_DragDropHelper, SHDRAGIMAGE}; + + use crate::definitions::{IDragSourceHelper, IDragSourceHelperVtbl, IID_IDragSourceHelper}; + + if width == 0 || height == 0 || rgba.len() != (width as usize) * (height as usize) * 4 { + return Err(E_FAIL); + } + + // Build a top-down 32bpp BGRA DIB. Top-down means negative biHeight; the shell helper + // expects BGRA byte order (B, G, R, A) with premultiplied alpha for smooth compositing. + let mut header: BITMAPINFO = unsafe { std::mem::zeroed() }; + header.bmiHeader = BITMAPINFOHEADER { + biSize: std::mem::size_of::() as u32, + biWidth: width as i32, + biHeight: -(height as i32), + biPlanes: 1, + biBitCount: 32, + biCompression: BI_RGB, + biSizeImage: width * height * 4, + biXPelsPerMeter: 0, + biYPelsPerMeter: 0, + biClrUsed: 0, + biClrImportant: 0, + }; + + let mut bits_ptr: *mut c_void = std::ptr::null_mut(); + let hbitmap = unsafe { + CreateDIBSection( + std::ptr::null_mut::() as HDC, + &header, + DIB_RGB_COLORS, + &mut bits_ptr, + std::ptr::null_mut(), + 0, + ) + }; + if hbitmap.is_null() || bits_ptr.is_null() { + return Err(E_FAIL); + } + + // Copy RGBA -> premultiplied BGRA so the shell can render the image with smooth alpha + // edges. Without premultiplication, the cursor preview shows a halo on every translucent + // pixel. + let dst = unsafe { std::slice::from_raw_parts_mut(bits_ptr as *mut u8, rgba.len()) }; + for (src_px, dst_px) in rgba.chunks_exact(4).zip(dst.chunks_exact_mut(4)) { + let (r, g, b, a) = (src_px[0] as u32, src_px[1] as u32, src_px[2] as u32, src_px[3]); + dst_px[0] = ((b * a as u32) / 255) as u8; + dst_px[1] = ((g * a as u32) / 255) as u8; + dst_px[2] = ((r * a as u32) / 255) as u8; + dst_px[3] = a; + } + + let mut helper: *mut IDragSourceHelper = std::ptr::null_mut(); + let hr = unsafe { + CoCreateInstance( + &CLSID_DragDropHelper, + std::ptr::null_mut(), + CLSCTX_ALL, + &IID_IDragSourceHelper, + &mut helper as *mut _ as *mut _, + ) + }; + if hr < 0 || helper.is_null() { + unsafe { DeleteObject(hbitmap as _) }; + return Err(hr); + } + + // The helper API takes `ptOffset` as the cursor's position inside the image (positive + // values into the image), but cross-platform `DragIcon::offset` is the icon-relative + // offset where the cursor sits (negative values mean the icon extends up/left of the + // cursor). Negate to translate between the two conventions. + let sdi = SHDRAGIMAGE { + sizeDragImage: windows_sys::Win32::Foundation::SIZE { cx: width as i32, cy: height as i32 }, + ptOffset: POINT { x: -offset_x, y: -offset_y }, + hbmpDragImage: hbitmap, + crColorKey: 0xffff_ffff, // CLR_NONE - use the alpha channel + }; + + let vtbl = unsafe { + &*((*(helper as *mut *mut IDragSourceHelperVtbl)) as *const IDragSourceHelperVtbl) + }; + let init_hr = unsafe { (vtbl.InitializeFromBitmap)(helper, &sdi, data_object) }; + let release = vtbl.parent.Release; + unsafe { release(helper as *mut IUnknown) }; + + if init_hr < 0 { + // On failure the helper did not take ownership of the bitmap - we still own it. + unsafe { DeleteObject(hbitmap as _) }; + return Err(init_hr); + } + // On success the helper stores the bitmap on the data object and will delete it later; + // do NOT call DeleteObject here. + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_offset(buf: &[u8], name: &str) -> usize { + let prefix = format!("{name}:"); + let head = std::str::from_utf8(buf).unwrap(); + let pos = head.find(&prefix).unwrap() + prefix.len(); + head[pos..pos + 10].parse().unwrap() + } + + #[test] + fn html_clipboard_format_brackets_user_html() { + let html = "Winit example"; + let buf = build_html_clipboard_format(html); + + assert!(buf.starts_with(b"Version:0.9\r\n")); + + let start_html = parse_offset(&buf, "StartHTML"); + let end_html = parse_offset(&buf, "EndHTML"); + let doc = std::str::from_utf8(&buf[start_html..end_html]).unwrap(); + assert!(doc.starts_with("")); + assert!(doc.ends_with("")); + + let start_fragment = parse_offset(&buf, "StartFragment"); + let end_fragment = parse_offset(&buf, "EndFragment"); + let fragment = std::str::from_utf8(&buf[start_fragment..end_fragment]).unwrap(); + assert_eq!(fragment, html); + } + + #[test] + fn html_clipboard_format_preserves_pre_wrapped() { + let pre_wrapped = + "

hi

"; + let buf = build_html_clipboard_format(pre_wrapped); + + let start_fragment = parse_offset(&buf, "StartFragment"); + let end_fragment = parse_offset(&buf, "EndFragment"); + let fragment = std::str::from_utf8(&buf[start_fragment..end_fragment]).unwrap(); + assert_eq!(fragment, "

hi

"); + } +} diff --git a/winit-win32/src/drop_handler.rs b/winit-win32/src/drop_handler.rs deleted file mode 100644 index 19a23c952..000000000 --- a/winit-win32/src/drop_handler.rs +++ /dev/null @@ -1,240 +0,0 @@ -use std::ffi::{OsString, c_void}; -use std::os::windows::ffi::OsStringExt; -use std::path::PathBuf; -use std::ptr; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use dpi::PhysicalPosition; -use tracing::debug; -use windows_sys::Win32::Foundation::{DV_E_FORMATETC, HWND, POINT, POINTL, S_OK}; -use windows_sys::Win32::Graphics::Gdi::ScreenToClient; -use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}; -use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE}; -use windows_sys::Win32::UI::Shell::{DragFinish, DragQueryFileW, HDROP}; -use windows_sys::core::{GUID, HRESULT}; -use winit_core::event::WindowEvent; - -use crate::definitions::{ - IDataObject, IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknown, IUnknownVtbl, -}; - -#[repr(C)] -pub struct FileDropHandlerData { - pub interface: IDropTarget, - refcount: AtomicUsize, - window: HWND, - send_event: Box, - cursor_effect: u32, - valid: bool, /* If the currently hovered item is not valid there must not be any - * `DragLeft` emitted */ -} - -pub struct FileDropHandler { - pub data: *mut FileDropHandlerData, -} - -#[allow(non_snake_case)] -impl FileDropHandler { - pub(crate) fn new(window: HWND, send_event: Box) -> FileDropHandler { - let data = Box::new(FileDropHandlerData { - interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl }, - refcount: AtomicUsize::new(1), - window, - send_event, - cursor_effect: DROPEFFECT_NONE, - valid: false, - }); - FileDropHandler { data: Box::into_raw(data) } - } - - // Implement IUnknown - pub unsafe extern "system" fn QueryInterface( - _this: *mut IUnknown, - _riid: *const GUID, - _ppvObject: *mut *mut c_void, - ) -> HRESULT { - // This function doesn't appear to be required for an `IDropTarget`. - // An implementation would be nice however. - unimplemented!(); - } - - pub unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 { - let drop_handler_data = unsafe { Self::from_interface(this) }; - let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1; - count as u32 - } - - pub unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 { - let drop_handler = unsafe { Self::from_interface(this) }; - let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1; - if count == 0 { - // Destroy the underlying data - drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) }); - } - count as u32 - } - - pub unsafe extern "system" fn DragEnter( - this: *mut IDropTarget, - pDataObj: *const IDataObject, - _grfKeyState: u32, - pt: POINTL, - pdwEffect: *mut u32, - ) -> HRESULT { - let drop_handler = unsafe { Self::from_interface(this) }; - let mut pt = POINT { x: pt.x, y: pt.y }; - unsafe { - ScreenToClient(drop_handler.window, &mut pt); - } - let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); - let mut paths = Vec::new(); - let hdrop = unsafe { Self::iterate_filenames(pDataObj, |path| paths.push(path)) }; - drop_handler.valid = hdrop.is_some(); - if drop_handler.valid { - (drop_handler.send_event)(WindowEvent::DragEntered { paths, position }); - } - drop_handler.cursor_effect = - if drop_handler.valid { DROPEFFECT_COPY } else { DROPEFFECT_NONE }; - unsafe { - *pdwEffect = drop_handler.cursor_effect; - } - - S_OK - } - - pub unsafe extern "system" fn DragOver( - this: *mut IDropTarget, - _grfKeyState: u32, - pt: POINTL, - pdwEffect: *mut u32, - ) -> HRESULT { - let drop_handler = unsafe { Self::from_interface(this) }; - if drop_handler.valid { - let mut pt = POINT { x: pt.x, y: pt.y }; - unsafe { - ScreenToClient(drop_handler.window, &mut pt); - } - let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); - (drop_handler.send_event)(WindowEvent::DragMoved { position }); - } - unsafe { - *pdwEffect = drop_handler.cursor_effect; - } - - S_OK - } - - pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT { - let drop_handler = unsafe { Self::from_interface(this) }; - if drop_handler.valid { - (drop_handler.send_event)(WindowEvent::DragLeft { position: None }); - } - - S_OK - } - - pub unsafe extern "system" fn Drop( - this: *mut IDropTarget, - pDataObj: *const IDataObject, - _grfKeyState: u32, - pt: POINTL, - pdwEffect: *mut u32, - ) -> HRESULT { - let drop_handler = unsafe { Self::from_interface(this) }; - if drop_handler.valid { - let mut pt = POINT { x: pt.x, y: pt.y }; - unsafe { - ScreenToClient(drop_handler.window, &mut pt); - } - let position = PhysicalPosition::new(pt.x as f64, pt.y as f64); - let mut paths = Vec::new(); - let hdrop = unsafe { Self::iterate_filenames(pDataObj, |path| paths.push(path)) }; - (drop_handler.send_event)(WindowEvent::DragDropped { paths, position }); - if let Some(hdrop) = hdrop { - unsafe { - DragFinish(hdrop); - } - } - } - unsafe { - *pdwEffect = drop_handler.cursor_effect; - } - - S_OK - } - - unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData { - unsafe { &mut *(this as *mut _) } - } - - unsafe fn iterate_filenames(data_obj: *const IDataObject, mut callback: F) -> Option - where - F: FnMut(PathBuf), - { - let drop_format = FORMATETC { - cfFormat: CF_HDROP, - ptd: ptr::null_mut(), - dwAspect: DVASPECT_CONTENT, - lindex: -1, - tymed: TYMED_HGLOBAL as u32, - }; - - let mut medium = unsafe { std::mem::zeroed() }; - let get_data_fn = unsafe { (*(*data_obj).cast::()).GetData }; - let get_data_result = unsafe { get_data_fn(data_obj as *mut _, &drop_format, &mut medium) }; - if get_data_result >= 0 { - let hdrop = unsafe { medium.u.hGlobal as HDROP }; - - // The second parameter (0xFFFFFFFF) instructs the function to return the item count - let item_count = unsafe { DragQueryFileW(hdrop, 0xffffffff, ptr::null_mut(), 0) }; - - for i in 0..item_count { - // Get the length of the path string NOT including the terminating null character. - // Previously, this was using a fixed size array of MAX_PATH length, but the - // Windows API allows longer paths under certain circumstances. - let character_count = - unsafe { DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize }; - let str_len = character_count + 1; - - // Fill path_buf with the null-terminated file name - let mut path_buf = Vec::with_capacity(str_len); - unsafe { - DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32); - path_buf.set_len(str_len); - } - - callback(OsString::from_wide(&path_buf[0..character_count]).into()); - } - - Some(hdrop) - } else if get_data_result == DV_E_FORMATETC { - // If the dropped item is not a file this error will occur. - // In this case it is OK to return without taking further action. - debug!("Error occurred while processing dropped/hovered item: item is not a file."); - None - } else { - debug!("Unexpected error occurred while processing dropped/hovered item."); - None - } - } -} - -impl Drop for FileDropHandler { - fn drop(&mut self) { - unsafe { - FileDropHandler::Release(self.data as *mut IUnknown); - } - } -} - -static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { - parent: IUnknownVtbl { - QueryInterface: FileDropHandler::QueryInterface, - AddRef: FileDropHandler::AddRef, - Release: FileDropHandler::Release, - }, - DragEnter: FileDropHandler::DragEnter, - DragOver: FileDropHandler::DragOver, - DragLeave: FileDropHandler::DragLeave, - Drop: FileDropHandler::Drop, -}; diff --git a/winit-win32/src/event_loop.rs b/winit-win32/src/event_loop.rs index 4be616435..74ce17076 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -63,6 +63,9 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{ }; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor, CustomCursorSource}; +use winit_core::data_transfer::{ + DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData, +}; use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; use winit_core::event::{ DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton, @@ -70,8 +73,8 @@ use winit_core::event::{ }; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, - EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider, + ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents, + DndAction, DragIcon, EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::keyboard::ModifiersState; @@ -82,8 +85,9 @@ pub(super) use self::runner::{Event, EventLoopRunner}; use super::SelectedCursor; use super::window::set_skip_taskbar; use crate::dark_mode::try_theme; +use crate::dnd::{DropSource, FileDropHandler, SourceDataObject, WinDataTransfer, WinTypedData}; use crate::dpi::{become_dpi_aware, dpi_to_scale_factor}; -use crate::drop_handler::FileDropHandler; +use crate::event_loop::runner::PendingDrag; use crate::icon::WinCursor; use crate::ime::ImeContext; use crate::keyboard::KeyEventBuilder; @@ -478,6 +482,105 @@ impl RootActiveEventLoop for ActiveEventLoop { fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let Some(state) = self.0.drag_state(id) else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + let hint = type_.hint().ok_or(RequestError::Ignored)?; + let typed_data = WinTypedData::new(state.data.clone(), hint) + .map(|value| Arc::new(value) as Arc) + .ok_or(RequestError::Ignored)?; + + let serial = AsyncRequestSerial::get(); + + self.0.send_event(Event::Window { + window_id: state.window_id, + event: WindowEvent::DataTransferReceived { id, serial, value: typed_data }, + }); + + Ok(serial) + } + + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let Some(state) = self.0.drag_state(id) else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + + Ok(Box::new(WinDataTransfer::new(state.data.clone()))) + } + + fn set_valid_dnd_actions( + &self, + id: DataTransferId, + actions: &[DndAction], + ) -> Result<(), RequestError> { + let mut state = self.0.drag_state.borrow_mut(); + let Some(state) = state.as_mut().filter(|s| s.id == id) else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + state.actions = actions.to_vec(); + Ok(()) + } + + fn start_drag( + &self, + source: WindowId, + send_data: Box, + allowed_actions: &[DndAction], + icon: Option, + ) -> Result { + let allowed_effects = crate::dnd::dnd_actions_to_dropeffect_mask(allowed_actions); + // Win32 would happily run a modal `DoDragDrop` with `allowed_effects == 0`, but every + // target would see "no action allowed" and the drag would end in a guaranteed cancel + // after burning a full modal pump. Fail fast instead - the caller asked for a drag + // they explicitly refuse to allow. + if allowed_effects == 0 { + return Err( + NotSupportedError::new("start_drag called with an empty action mask").into() + ); + } + + let id = crate::dnd::next_data_transfer_id(); + let data_object = SourceDataObject::new(send_data); + let drop_source = DropSource::new(); + + // Attach a drag preview if the app supplied one. Cosmetic failures must not abort the + // drag - the gesture still works, just without a custom image - so log and move on. + if let Some(icon) = icon { + if let Some(rgba) = icon.icon.cast_ref::() { + let result = unsafe { + crate::dnd::apply_drag_image( + data_object.interface_ptr() as *mut _, + rgba.width(), + rgba.height(), + rgba.buffer(), + icon.offset_x, + icon.offset_y, + ) + }; + if let Err(hr) = result { + tracing::warn!("Failed to attach drag image: hr=0x{hr:08x}"); + } + } else { + tracing::warn!("DragIcon::icon must be an RgbaIcon on win32; ignoring"); + } + } + + self.0.pending_drag.replace(Some(PendingDrag { + window_id: source, + id, + data_object, + drop_source, + allowed_effects, + })); + + Ok(id) + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { @@ -487,6 +590,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop { } } +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + #[derive(Clone)] pub(crate) struct OwnedDisplayHandle; @@ -1177,6 +1293,14 @@ unsafe fn public_window_callback_inner( result = ProcResult::Value(0); }, + WM_PAINT if userdata.event_loop_runner.source_drag.get().is_some() => { + // While a source-side drag is in flight, the app handler is on the stack (we're + // inside `start_drag` -> `DoDragDrop`), so we can neither dispatch `RedrawRequested` + // nor keep re-arming via `RDW_INTERNALPAINT` (that would spin in OLE's modal loop). + // Let `DefWindowProcW` validate the region and show stale content for the duration of + // the drag; the next real paint happens once `DoDragDrop` returns. + result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) }); + }, WM_PAINT => { userdata.window_state_lock().redraw_requested = userdata.event_loop_runner.should_buffer(); @@ -2352,6 +2476,13 @@ unsafe fn public_window_callback_inner( .catch_unwind(callback) .unwrap_or_else(|| result = ProcResult::Value(-1)); + // We execute a new drag operation here instead of immediately starting it in + // `ActiveEventLoop::start_drag`. `DoDragDrop` is blocking and synchronous, so if we started + // it inside the event loop then an internal drag operation would be re-entrant and the + // application would not be able to handle the incoming messages. This is after the application + // has had a chance to handle mouse events. + userdata.event_loop_runner.try_execute_drag_drop(); + match result { ProcResult::DefWindowProc(wparam) => unsafe { DefWindowProcW(window, msg, wparam, lparam) }, ProcResult::Value(val) => val, diff --git a/winit-win32/src/event_loop/runner.rs b/winit-win32/src/event_loop/runner.rs index 65e608276..241acbeb6 100644 --- a/winit-win32/src/event_loop/runner.rs +++ b/winit-win32/src/event_loop/runner.rs @@ -1,5 +1,5 @@ use std::any::Any; -use std::cell::{Cell, RefCell}; +use std::cell::{Cell, Ref, RefCell}; use std::collections::VecDeque; use std::rc::Rc; use std::sync::{Arc, Mutex}; @@ -7,18 +7,49 @@ use std::time::Instant; use std::{fmt, mem, panic}; use dpi::PhysicalSize; -use windows_sys::Win32::Foundation::HWND; +use windows_sys::Win32::Foundation::{DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, HWND}; +use windows_sys::Win32::System::Ole::{ + DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, DoDragDrop, +}; use winit_core::application::ApplicationHandler; +use winit_core::data_transfer::DataTransferId; use winit_core::event::{DeviceEvent, DeviceId, StartCause, SurfaceSizeWriter, WindowEvent}; -use winit_core::event_loop::ActiveEventLoop as RootActiveEventLoop; +use winit_core::event_loop::{ActiveEventLoop as RootActiveEventLoop, DndAction}; use winit_core::window::WindowId; use super::{ActiveEventLoop, ControlFlow, EventLoopThreadExecutor}; +use crate::dnd::{DataObject, DropEffect, DropSource, SourceDataObject, drop_effect_to_dnd_action}; use crate::event_loop::{GWL_USERDATA, WindowData}; use crate::util::get_window_long; type EventHandler = Cell>; +/// State for the single drag-and-drop transfer currently in flight (OLE guarantees at most one +/// active drag per process). +#[derive(Debug)] +pub(super) struct DragState { + pub(super) id: DataTransferId, + pub(super) window_id: WindowId, + pub(super) data: Arc, + pub(super) actions: Vec, +} + +pub(super) struct PendingDrag { + pub(super) window_id: WindowId, + pub(super) data_object: SourceDataObject, + pub(super) drop_source: DropSource, + pub(super) allowed_effects: DropEffect, + pub(super) id: DataTransferId, +} + +/// Set while `DoDragDrop` is on the call stack - i.e., this process is the source of an active +/// drag. The target-side `IDropTarget` checks this to recognize self-drops and reuse the source's +/// id + allowed actions instead of waiting for the (buffered) app `DragEntered` handler. +#[derive(Copy, Clone)] +pub(crate) struct SourceDrag { + pub(crate) id: DataTransferId, +} + pub(crate) struct EventLoopRunner { pub(super) thread_id: u32, @@ -37,6 +68,29 @@ pub(crate) struct EventLoopRunner { event_handler: Rc, event_buffer: RefCell>, + /// The currently in-flight drag transfer, if any, alive between `DragEntered` and + /// `DragLeft`/`DragDropped`. + pub(super) drag_state: RefCell>, + + /// `Some(_)` while `start_drag` has `DoDragDrop` on the call stack. + pub(crate) source_drag: Cell>, + + /// `DoDragDrop` is blocking and synchronous, so we wait until after the application returns + /// control to winit before actually calling into the OS to initiate the drag. This prevents + /// the event loop from being re-entrant if we are doing an internal drag operation, since if + /// we handled this inside `ActiveEventLoop::start_drag` then all the `WindowEvent::Drag*` + /// events would be buffered until `DoDragDrop` returns, preventing the application from + /// handling those messages. + pub(super) pending_drag: RefCell>, + + /// For self-drops, target-side `IDropTarget::Drop` can't release the cached `DragState` + /// before its `DragDropped` `WindowEvent` is delivered - the event is buffered (the outer app + /// handler holds `event_handler` for the duration of `DoDragDrop`) and `data_transfer(id)` + /// would return `UnknownDataTransfer` if cleanup ran synchronously. So we stash the id here + /// and drain it at the end of `dispatch_buffered_events`, after the app's buffered handler + /// has had its chance to read the data. + pending_source_drag_cleanup: Cell>, + panic_error: Cell>, } @@ -87,9 +141,122 @@ impl EventLoopRunner { last_events_cleared: Cell::new(Instant::now()), event_handler: Rc::new(Cell::new(None)), event_buffer: RefCell::new(VecDeque::new()), + drag_state: RefCell::new(None), + source_drag: Cell::new(None), + pending_drag: RefCell::new(None), + pending_source_drag_cleanup: Cell::new(None), } } + pub(super) fn try_execute_drag_drop(self: &Rc) { + let Some(PendingDrag { data_object, drop_source, id, allowed_effects, window_id }) = + self.pending_drag.take() + else { + return; + }; + + // Make the drag visible to our own target-side `IDropTarget` so it can recognize + // self-drops and reuse this id + action mask without going through the (buffered) app + // handler. The guard ensures the flag is cleared on any exit path - if anything between + // here and `DoDragDrop`'s return panics, the stale flag would otherwise permanently + // disable `WM_PAINT` dispatch and misclassify all future external drags as self-drops. + struct ClearOnDrop<'a>(&'a Cell>); + impl Drop for ClearOnDrop<'_> { + fn drop(&mut self) { + self.0.set(None); + } + } + self.source_drag.set(Some(SourceDrag { id })); + let _guard = ClearOnDrop(&self.source_drag); + + let mut effect_out: u32 = DROPEFFECT_NONE; + let hr = unsafe { + DoDragDrop( + data_object.interface_ptr(), + drop_source.interface_ptr(), + allowed_effects, + &mut effect_out, + ) + }; + + if hr == DRAGDROP_S_DROP { + let action = drop_effect_to_dnd_action(effect_out); + + self.send_event(Event::Window { + window_id, + event: WindowEvent::OutgoingDragDropped { id, action }, + }); + } else if hr == DRAGDROP_S_CANCEL { + self.send_event(Event::Window { + window_id, + event: WindowEvent::OutgoingDragCanceled { id }, + }); + } else { + tracing::error!("DoDragDrop failed: 0x{hr:08x}"); + return; + } + + // Both `DRAGDROP_S_DROP` and `DRAGDROP_S_CANCEL` are success codes for us - the app + // will hear about the outcome via the buffered `DragDropped`/`DragLeft` events + // (target-side translates `effect_out == DROPEFFECT_NONE` to `DragLeft`). + // Log the negotiated effect so cross-process drops, which have no target-side event + // in this process, leave a debuggable trace of what action the remote target performed. + tracing::trace!( + "DoDragDrop completed: hr=0x{hr:08x} effect_out={effect_out} (COPY={DROPEFFECT_COPY}, \ + MOVE={DROPEFFECT_MOVE}, LINK={DROPEFFECT_LINK})", + ); + } + + pub(crate) fn defer_source_drag_cleanup(&self, id: DataTransferId) { + self.pending_source_drag_cleanup.set(Some(id)); + } + + pub(crate) fn register_data_transfer( + &self, + id: DataTransferId, + window_id: WindowId, + data: Arc, + ) { + // By default, no actions have been set as valid by the target. + *self.drag_state.borrow_mut() = + Some(DragState { id, window_id, data, actions: Default::default() }); + } + + pub(crate) fn remove_data_transfer(&self, id: DataTransferId) { + let mut state = self.drag_state.borrow_mut(); + if state.as_ref().is_some_and(|s| s.id == id) { + *state = None; + } + } + + pub(super) fn drag_state(&self, id: DataTransferId) -> Option> { + Ref::filter_map(self.drag_state.borrow(), |state| state.as_ref().filter(|s| s.id == id)) + .ok() + } + + pub(crate) fn current_drag_actions(&self, id: DataTransferId) -> Ref<'_, [DndAction]> { + Ref::map(self.drag_state.borrow(), |state| { + state.as_ref().filter(|s| s.id == id).map(|s| &s.actions[..]).unwrap_or_default() + }) + } + + pub(crate) fn proposed_dnd_action( + &self, + id: DataTransferId, + effects: DropEffect, + ) -> Option { + self.current_drag_actions(id).iter().copied().find(|action| { + let effect = match action { + DndAction::Move => DROPEFFECT_MOVE, + DndAction::Copy => DROPEFFECT_COPY, + DndAction::Link => DROPEFFECT_LINK, + _ => return false, + }; + + (effects & effect) != 0 + }) + } + /// Associate the application's event handler with the runner. /// /// # Safety @@ -138,12 +305,20 @@ impl EventLoopRunner { last_events_cleared: _, event_handler, event_buffer: _, + drag_state, + source_drag, + pending_drag, + pending_source_drag_cleanup, } = self; interrupt_msg_dispatch.set(false); runner_state.set(RunnerState::Uninitialized); panic_error.set(None); exit.set(None); event_handler.set(None); + drag_state.take(); + source_drag.set(None); + pending_drag.take(); + pending_source_drag_cleanup.set(None); } } @@ -285,6 +460,11 @@ impl EventLoopRunner { None => break, } } + // The app's buffered `DragDropped` handler (if any) has now had its chance to call + // `data_transfer(id)`; safe to release the cached `DragState` for a deferred self-drop. + if let Some(id) = self.pending_source_drag_cleanup.take() { + self.remove_data_transfer(id); + } } /// Dispatch control flow events (`NewEvents`, `AboutToWait`, and diff --git a/winit-win32/src/lib.rs b/winit-win32/src/lib.rs index 569d1f32c..4693875b4 100644 --- a/winit-win32/src/lib.rs +++ b/winit-win32/src/lib.rs @@ -8,8 +8,8 @@ mod util; mod dark_mode; mod definitions; +mod dnd; mod dpi; -mod drop_handler; mod event_loop; mod icon; mod ime; diff --git a/winit-win32/src/window.rs b/winit-win32/src/window.rs index dba95e9bd..34e6f83a3 100644 --- a/winit-win32/src/window.rs +++ b/winit-win32/src/window.rs @@ -61,8 +61,8 @@ use crate::dark_mode::try_theme; use crate::definitions::{ CLSID_TaskbarList, IID_ITaskbarList, IID_ITaskbarList2, ITaskbarList, ITaskbarList2, }; +use crate::dnd::FileDropHandler; use crate::dpi::{dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi}; -use crate::drop_handler::FileDropHandler; use crate::event_loop::{self, ActiveEventLoop, DESTROY_MSG_ID, Event, EventLoopRunner}; use crate::icon::{IconType, WinCursor}; use crate::ime::ImeContext; @@ -1191,6 +1191,7 @@ impl InitData<'_> { let window_state = { let window_state = WindowState::new( &self.attributes, + &self.win_attributes, scale_factor, current_theme, self.attributes.preferred_theme, @@ -1230,15 +1231,16 @@ impl InitData<'_> { let file_drop_runner = self.runner.clone(); let window_id = win.id(); - let file_drop_handler = FileDropHandler::new( + let mut file_drop_handler = FileDropHandler::new( win.window.hwnd(), + self.runner.clone(), Box::new(move |event| { file_drop_runner.send_event(Event::Window { window_id, event }) }), ); let handler_interface_ptr = - unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void }; + unsafe { file_drop_handler.interface_unchecked_mut() as *mut _ as *mut c_void }; assert_eq!(unsafe { RegisterDragDrop(win.window.hwnd(), handler_interface_ptr) }, S_OK); Some(file_drop_handler) diff --git a/winit-win32/src/window_state.rs b/winit-win32/src/window_state.rs index ed06d7604..eefb654b0 100644 --- a/winit-win32/src/window_state.rs +++ b/winit-win32/src/window_state.rs @@ -22,7 +22,7 @@ use winit_core::keyboard::ModifiersState; use winit_core::monitor::Fullscreen; use winit_core::window::{ImeCapabilities, Theme, WindowAttributes}; -use crate::{SelectedCursor, event_loop, util}; +use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util}; /// Contains information about states and the window that the callback is going to use. #[derive(Debug)] @@ -154,6 +154,7 @@ pub enum ImeState { impl WindowState { pub(crate) fn new( attributes: &WindowAttributes, + _win_attributes: &WindowAttributesWindows, scale_factor: f64, current_theme: Theme, preferred_theme: Option, diff --git a/winit-x11/src/atoms.rs b/winit-x11/src/atoms.rs index e5ac78f4d..ecbbbb129 100644 --- a/winit-x11/src/atoms.rs +++ b/winit-x11/src/atoms.rs @@ -13,7 +13,7 @@ macro_rules! atom_manager { /// Indices into the `Atoms` struct. #[derive(Copy, Clone, Debug)] - #[allow(non_camel_case_types)] + #[allow(non_camel_case_types, clippy::upper_case_acronyms)] pub enum AtomName { $($name,)* } @@ -34,6 +34,7 @@ macro_rules! atom_manager { atom_manager! { // General Use Atoms CARD32, + STRING, UTF8_STRING, WM_CHANGE_STATE, WM_CLIENT_MACHINE, @@ -87,11 +88,39 @@ atom_manager! { XdndDrop, XdndPosition, XdndStatus, - XdndActionPrivate, + XdndActionCopy, XdndSelection, XdndFinished, XdndTypeList, + + // MIME types for reading selections TextUriList: b"text/uri-list", + TextPlain: b"text/plain", + TextPlainCharsetUtf8: b"text/plain;charset=utf-8", + TextHtml: b"text/html", + TextHtmlCharsetUtf8: b"text/html;charset=utf-8", + ApplicationRtf: b"application/rtf", + AudioAac: b"audio/aac", + AudioAiff: b"audio/aiff", + AudioFlac: b"audio/flac", + AudioWav: b"audio/wav", + AudioWave: b"audio/wave", + AudioXWav: b"audio/x-wav", + AudioVndWav: b"audio/vnd.wav", + AudioVndWave: b"audio/vnd.wave", + AudioMpeg: b"audio/mpeg", + AudioOgg: b"audio/ogg", + ImageBmp: b"image/bmp", + ImageGif: b"image/gif", + ImageJpeg: b"image/jpeg", + ImagePjpeg: b"image/pjpeg", + ImagePng: b"image/png", + ImageSvg: b"image/svg+xml", + ImageTiff: b"image/tiff", + ImageWebp: b"image/webp", + ImageXIcon: b"image/x-icon", + ImageRaw: b"image/x-panasonic-raw", + None: b"None", // Miscellaneous Atoms diff --git a/winit-x11/src/dnd.rs b/winit-x11/src/dnd.rs index 3c85e4fe2..af4f86eca 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -1,18 +1,19 @@ +use std::collections::VecDeque; use std::io; use std::os::raw::*; -use std::path::{Path, PathBuf}; use std::str::Utf8Error; use std::sync::Arc; +use std::sync::atomic::{AtomicI64, Ordering}; -use dpi::PhysicalPosition; -use percent_encoding::percent_decode; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; +use winit_core::event_loop::AsyncRequestSerial; use x11rb::protocol::xproto::{self, ConnectionExt}; use crate::atoms::AtomName::None as DndNone; use crate::atoms::*; use crate::event_loop::{CookieResultExt, X11Error}; -use crate::util; use crate::xdisplay::XConnection; +use crate::{XWindow, util}; #[derive(Debug, Clone, Copy)] pub enum DndState { @@ -21,98 +22,264 @@ pub enum DndState { } #[derive(Debug)] -pub enum DndDataParseError { +pub enum UriListParseError { EmptyData, InvalidUtf8(#[allow(dead_code)] Utf8Error), HostnameSpecified(#[allow(dead_code)] String), UnexpectedProtocol(#[allow(dead_code)] String), UnresolvablePath(#[allow(dead_code)] io::Error), + Io(#[allow(dead_code)] io::Error), } -impl From for DndDataParseError { +impl From for UriListParseError { fn from(e: Utf8Error) -> Self { - DndDataParseError::InvalidUtf8(e) + UriListParseError::InvalidUtf8(e) } } -impl From for DndDataParseError { +impl From for UriListParseError { fn from(e: io::Error) -> Self { - DndDataParseError::UnresolvablePath(e) + UriListParseError::UnresolvablePath(e) + } +} + +#[derive(Debug)] +pub struct SelectionReader { + type_: SelectionType, + data: Vec, +} + +impl TypedData for SelectionReader { + fn try_read(&self) -> Option> { + Some(Box::new(io::Cursor::new(self.data.clone()))) + } + + fn type_(&self) -> &dyn TransferType { + &self.type_ + } + + fn try_as_string(&self) -> io::Result { + fn invalid_data(err: E) -> io::Error + where + E: Into>, + { + io::Error::new(io::ErrorKind::InvalidData, err) + } + + fn decode_utf16_bytes(bytes: &[u8]) -> io::Result { + let utf16 = bytes + .chunks_exact(2) + .map(|chunk| { + let bytes: &[u8; 2] = chunk.try_into().unwrap(); + u16::from_ne_bytes(*bytes) + }) + .collect::>(); + String::from_utf16(&utf16).map_err(invalid_data) + } + + match self.type_.hint() { + Some(TypeHint::Plaintext) | Some(TypeHint::Html) => std::str::from_utf8(&self.data) + .map(|str| str.to_owned()) + .map_err(invalid_data) + .or_else(|_| decode_utf16_bytes(&self.data)), + Some(TypeHint::UriList) => String::from_utf8(self.data.clone()).map_err(invalid_data), + _ => Err(io::ErrorKind::InvalidData.into()), + } + } + + fn try_as_uris(&self) -> io::Result> { + if self.type_().hint() != Some(TypeHint::UriList) { + return Err(io::ErrorKind::InvalidData.into()); + } + + Ok(self + .try_as_string()? + .split(['\n', '\r']) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned) + .collect()) + } +} + +#[derive(Debug)] +pub struct DragState { + // Populated by XdndEnter event handler + pub version: c_long, + pub transfer_id: DataTransferId, + pub types: Arc<[SelectionType]>, + // Populated by Xdnd* event handlers + pub source_window: xproto::Window, + // Populated by Xdnd* event handlers + pub target_window: xproto::Window, + // Populated by `fetch_data_transfer` + pub pending_fetch_types: VecDeque<(AsyncRequestSerial, SelectionType)>, + pub finished: Option<(XWindow, XWindow)>, + /// Whether the drag operation is accepted (or `None` if the user never indicated that it's + /// accepted or rejected) + // Populated by `Window::accept_drag`/`Window::reject_drag`. + pub accepted: bool, +} + +impl Default for DragState { + fn default() -> Self { + static DATA_TRANSFER_ID: AtomicI64 = AtomicI64::new(0); + + Self { + version: Default::default(), + transfer_id: DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)), + types: Default::default(), + source_window: Default::default(), + target_window: Default::default(), + pending_fetch_types: Default::default(), + finished: None, + accepted: Default::default(), + } } } #[derive(Debug)] pub struct Dnd { xconn: Arc, - // Populated by XdndEnter event handler - pub version: Option, - pub type_list: Option>, - // Populated by XdndPosition event handler - pub source_window: Option, - // Populated by XdndPosition event handler - pub position: PhysicalPosition, - // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) - pub result: Option, DndDataParseError>>, - // Populated by SelectionNotify event handler (triggered by XdndPosition event handler) - pub dragging: bool, + // If `None`, no drag operation is in progress. + state: Option, +} + +#[derive(Debug)] +pub struct Selection { + types: Arc<[SelectionType]>, +} + +impl Selection { + pub(crate) fn new(types: Arc<[SelectionType]>) -> Selection { + Selection { types } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SelectionType { + hint: Option, + atom: xproto::Atom, +} + +impl SelectionType { + pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self { + let atom_to_hint = [ + // Files + (atoms[TextUriList], TypeHint::UriList), + // Plaintext + (atoms[STRING], TypeHint::Plaintext), + (atoms[UTF8_STRING], TypeHint::Plaintext), + (atoms[TextPlain], TypeHint::Plaintext), + (atoms[TextPlainCharsetUtf8], TypeHint::Plaintext), + // HTML + (atoms[TextHtml], TypeHint::Html), + (atoms[TextHtmlCharsetUtf8], TypeHint::Html), + // RTF + (atoms[ApplicationRtf], TypeHint::Rtf), + // Audio + (atoms[AudioAac], TypeHint::Audio { extension_hint: Some("aac") }), + (atoms[AudioAiff], TypeHint::Audio { extension_hint: Some("aif") }), + (atoms[AudioFlac], TypeHint::Audio { extension_hint: Some("flac") }), + (atoms[AudioVndWav], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioVndWave], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioWav], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioWave], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioXWav], TypeHint::Audio { extension_hint: Some("wav") }), + (atoms[AudioOgg], TypeHint::Audio { extension_hint: Some("ogg") }), + (atoms[AudioMpeg], TypeHint::Audio { extension_hint: Some("mp3") }), + // Image + (atoms[ImageBmp], TypeHint::Image { extension_hint: Some("bmp") }), + (atoms[ImageGif], TypeHint::Image { extension_hint: Some("gif") }), + (atoms[ImageJpeg], TypeHint::Image { extension_hint: Some("jpg") }), + (atoms[ImagePjpeg], TypeHint::Image { extension_hint: Some("jpg") }), + (atoms[ImagePng], TypeHint::Image { extension_hint: Some("png") }), + (atoms[ImageRaw], TypeHint::Image { extension_hint: Some("raw") }), + (atoms[ImageSvg], TypeHint::Image { extension_hint: Some("svg") }), + (atoms[ImageTiff], TypeHint::Image { extension_hint: Some("tiff") }), + (atoms[ImageWebp], TypeHint::Image { extension_hint: Some("webp") }), + (atoms[ImageXIcon], TypeHint::Image { extension_hint: Some("ico") }), + ]; + let hint = + atom_to_hint.iter().find_map(|(haystack, hint)| (*haystack == atom).then_some(*hint)); + + Self { hint, atom } + } + + pub fn atom(&self) -> xproto::Atom { + self.atom + } +} + +impl TransferType for SelectionType { + fn hint(&self) -> Option { + self.hint + } + + fn matches(&self, other: &dyn TransferType) -> bool { + if let Some(other_mime) = other.cast_ref::() { + *self == *other_mime + } else { + // If either hint is `None`, return false + self.hint().is_some_and(|hint| other.hint() == Some(hint)) + } + } +} + +impl DataTransfer for Selection { + fn for_each_available_type<'this>( + &'this self, + func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>, + ) { + let _ = self.types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func); + } } impl Dnd { - pub fn new(xconn: Arc) -> Result { - Ok(Dnd { - xconn, - version: None, - type_list: None, - source_window: None, - position: PhysicalPosition::default(), - result: None, - dragging: false, - }) + pub fn new(xconn: Arc) -> Self { + Dnd { xconn, state: None } } - pub fn reset(&mut self) { - self.version = None; - self.type_list = None; - self.source_window = None; - self.result = None; - self.dragging = false; + pub fn state(&self) -> Option<&DragState> { + self.state.as_ref() } - pub unsafe fn send_status( - &self, - this_window: xproto::Window, + pub fn state_mut(&mut self) -> Option<&mut DragState> { + self.state.as_mut() + } + + pub fn find_type_by_hint(&self, hint: TypeHint) -> Option<&SelectionType> { + self.state.as_ref()?.types.iter().find(|haystack| haystack.hint() == Some(hint)) + } + + pub fn init_state( + &mut self, + version: c_long, + source_window: xproto::Window, target_window: xproto::Window, - state: DndState, - ) -> Result<(), X11Error> { - let atoms = self.xconn.atoms(); - let (accepted, action) = match state { - DndState::Accepted => (1, atoms[XdndActionPrivate]), - DndState::Rejected => (0, atoms[DndNone]), - }; - self.xconn - .send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [ - this_window, - accepted, - 0, - 0, - action as _, - ])? - .ignore_error(); - - Ok(()) + types: Arc<[SelectionType]>, + ) -> &DragState { + self.state.get_or_insert(DragState { + version, + types, + source_window, + target_window, + ..Default::default() + }) } pub unsafe fn send_finished( &self, this_window: xproto::Window, target_window: xproto::Window, - state: DndState, ) -> Result<(), X11Error> { let atoms = self.xconn.atoms(); - let (accepted, action) = match state { - DndState::Accepted => (1, atoms[XdndActionPrivate]), - DndState::Rejected => (0, atoms[DndNone]), + let Some(state) = &self.state else { + return Err(X11Error::UnexpectedNull( + "Drag-and-drop state was not initialized (called `send_finished` before XdndEnter", + )); }; + let (accepted, action) = + if state.accepted { (1, atoms[XdndActionCopy]) } else { (0, atoms[DndNone]) }; self.xconn .send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [ this_window, @@ -138,54 +305,54 @@ impl Dnd { ) } - pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) { + pub fn convert_selection( + &self, + window: xproto::Window, + time: xproto::Timestamp, + new_type: xproto::Atom, + ) { let atoms = self.xconn.atoms(); self.xconn .xcb_connection() - .convert_selection( - window, - atoms[XdndSelection], - atoms[TextUriList], - atoms[XdndSelection], - time, - ) + // TODO: We store the converted selection back to `XdndSelection`. We should store to + // some new place so that `XdndSelection` remains untouched. + .convert_selection(window, atoms[XdndSelection], new_type, atoms[XdndSelection], time) .expect_then_ignore_error("Failed to send XdndSelection event") } - pub unsafe fn read_data( + pub unsafe fn send_status( + &self, + this_window: xproto::Window, + target_window: xproto::Window, + status: DndState, + ) -> Result<(), X11Error> { + let atoms = self.xconn.atoms(); + let (accepted, action) = match status { + DndState::Accepted => (1, atoms[XdndActionCopy]), + DndState::Rejected => (0, atoms[DndNone]), + }; + self.xconn + .send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [ + this_window, + accepted, + 0, + 0, + action as _, + ])? + .ignore_error(); + + Ok(()) + } + + pub fn read_data( &self, window: xproto::Window, - ) -> Result, util::GetPropertyError> { + type_: SelectionType, + ) -> Result { let atoms = self.xconn.atoms(); - self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList]) - } + let type_atom = type_.atom(); + let bytes = self.xconn.get_property(window, atoms[XdndSelection], type_atom)?; - pub fn parse_data(&self, data: &mut [c_uchar]) -> Result, DndDataParseError> { - if !data.is_empty() { - let mut path_list = Vec::new(); - let decoded = percent_decode(data).decode_utf8()?.into_owned(); - for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) { - // The format is specified as protocol://host/path - // However, it's typically simply protocol:///path - let path_str = if uri.starts_with("file://") { - let path_str = uri.replace("file://", ""); - if !path_str.starts_with('/') { - // A hostname is specified - // Supporting this case is beyond the scope of my mental health - return Err(DndDataParseError::HostnameSpecified(path_str)); - } - path_str - } else { - // Only the file protocol is supported - return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned())); - }; - - let path = Path::new(&path_str).canonicalize()?; - path_list.push(path); - } - Ok(path_list) - } else { - Err(DndDataParseError::EmptyData) - } + Ok(SelectionReader { type_, data: bytes }) } } diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index b89a06c1e..990be32be 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -19,12 +19,13 @@ use tracing::warn; use winit_common::xkb::Context; use winit_core::application::ApplicationHandler; use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource}; -use winit_core::error::{EventLoopError, RequestError}; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType}; +use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; use winit_core::event::{DeviceId, StartCause, WindowEvent}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, - EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider, + ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents, + DndAction, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; @@ -36,14 +37,17 @@ use x11rb::protocol::{xkb, xproto}; use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; -use crate::atoms::*; +use crate::atoms::{ + _NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms, + WM_DELETE_WINDOW, +}; use crate::dnd::Dnd; use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN}; use crate::ime::{self, Ime, ImeCreationError, ImeSender}; use crate::util::{self, CustomCursor}; use crate::window::{UnownedWindow, Window}; use crate::xdisplay::{XConnection, XError, XNotSupported}; -use crate::{XlibErrorHook, ffi, xsettings}; +use crate::{Selection, SelectionType, XlibErrorHook, ffi, xsettings}; // Xinput constants not defined in x11rb pub(crate) const ALL_DEVICES: u16 = 0; @@ -168,6 +172,7 @@ impl PeekableReceiver { #[derive(Debug)] pub struct ActiveEventLoop { pub(crate) xconn: Arc, + pub(crate) dnd: RefCell, pub(crate) wm_delete_window: xproto::Atom, pub(crate) net_wm_ping: xproto::Atom, pub(crate) net_wm_sync_request: xproto::Atom, @@ -226,8 +231,7 @@ impl EventLoop { let net_wm_ping = atoms[_NET_WM_PING]; let net_wm_sync_request = atoms[_NET_WM_SYNC_REQUEST]; - let dnd = Dnd::new(Arc::clone(&xconn)) - .expect("Failed to call XInternAtoms when initializing drag and drop"); + let dnd = Dnd::new(Arc::clone(&xconn)).into(); let (ime_sender, ime_receiver) = mpsc::channel(); let (ime_event_sender, ime_event_receiver) = mpsc::channel(); @@ -342,6 +346,7 @@ impl EventLoop { let window_target = ActiveEventLoop { ime, + dnd, root, control_flow: Cell::new(ControlFlow::default()), exit: Cell::new(None), @@ -368,7 +373,6 @@ impl EventLoop { let event_processor = EventProcessor { target: window_target, - dnd, devices: Default::default(), randr_event_offset, ime_receiver, @@ -621,9 +625,8 @@ impl EventLoop { fn drain_events(&mut self, app: &mut A) { let mut xev = MaybeUninit::uninit(); - while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } { - let mut xev = unsafe { xev.assume_init() }; - self.event_processor.process_event(&mut xev, app); + while let Some(xev) = self.event_processor.poll_one_event(&mut xev) { + self.event_processor.process_event(xev, app); } } @@ -759,6 +762,90 @@ impl RootActiveEventLoop for ActiveEventLoop { fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle { self } + + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let dnd = self.dnd.borrow(); + + if dnd.state().is_none_or(|state| state.transfer_id != id) { + return Err(RequestError::Ignored); + } + + let Some(state) = dnd.state() else { + return Err(RequestError::Ignored); + }; + + Ok(Box::new(Selection::new(state.types.clone()))) + } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result { + let mut dnd = self.dnd.borrow_mut(); + + let serial = AsyncRequestSerial::get(); + + let type_ = type_ + .cast_ref::() + .or_else(|| dnd.find_type_by_hint(type_.hint()?)) + .cloned() + .ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?; + + let new_convert_selection = { + let Some(state) = dnd.state_mut() else { + return Err(RequestError::Ignored); + }; + + if state.transfer_id != id { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown data transfer", + ))); + } + + // If it's non-empty, assume that we're still waiting on some other fetch operation. + // The `SelectionNotify` handler will send a new `convert_selection` event if any + // more are on the stack. + let should_emit_convert_selection = state.pending_fetch_types.is_empty(); + + let atom = type_.atom(); + + state.pending_fetch_types.push_back((serial, type_)); + + should_emit_convert_selection.then_some(( + state.target_window, + self.xconn.timestamp(), + atom, + )) + }; + + if let Some((window, time, new_type)) = new_convert_selection { + // This results in the `SelectionNotify` event + dnd.convert_selection(window, time, new_type); + } + + Ok(serial) + } + + fn set_valid_dnd_actions( + &self, + id: DataTransferId, + actions: &[DndAction], + ) -> Result<(), RequestError> { + let mut dnd = self.dnd.borrow_mut(); + + let Some(state) = dnd.state_mut() else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + + if state.transfer_id != id { + return Err(os_error!(UnknownDataTransfer(id)).into()); + } + + state.accepted = !actions.is_empty(); + + Ok(()) + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { @@ -767,6 +854,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop { } } +/// An operation was attempted on a data transfer ID, but that ID was invalid. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct UnknownDataTransfer(pub DataTransferId); + +impl fmt::Display for UnknownDataTransfer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let id = self.0.into_raw(); + write!(f, "Unknown data transfer with ID {id}") + } +} + +impl std::error::Error for UnknownDataTransfer {} + pub(crate) struct DeviceInfo<'a> { xconn: &'a XConnection, info: *const ffi::XIDeviceInfo, diff --git a/winit-x11/src/event_processor.rs b/winit-x11/src/event_processor.rs index 4127c01a6..425c904a4 100644 --- a/winit-x11/src/event_processor.rs +++ b/winit-x11/src/event_processor.rs @@ -1,10 +1,12 @@ use std::cell::{Cell, RefCell}; use std::collections::{HashMap, VecDeque}; +use std::mem::MaybeUninit; use std::os::raw::{c_char, c_int, c_long, c_ulong}; use std::slice; use std::sync::{Arc, Mutex}; use dpi::{PhysicalPosition, PhysicalSize}; +use tracing::warn; use winit_common::xkb::{self, Context, XkbState}; use winit_core::application::ApplicationHandler; use winit_core::event::{ @@ -12,6 +14,7 @@ use winit_core::event::{ MouseScrollDelta, PointerKind, PointerSource, RawKeyEvent, SurfaceSizeWriter, TouchPhase, WindowEvent, }; +use winit_core::event_loop::DndAction; use winit_core::keyboard::ModifiersState; use winit_core::window::WindowId; use x11_dl::xinput2::{ @@ -31,7 +34,7 @@ use x11rb::x11_utils::{ExtensionInformation, Serialize}; use xkbcommon_dl::xkb_mod_mask_t; use crate::atoms::*; -use crate::dnd::{Dnd, DndState}; +use crate::dnd::{DndState, SelectionType}; use crate::event_loop::{ ALL_DEVICES, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType, ScrollOrientation, mkdid, mkwid, @@ -49,7 +52,6 @@ const KEYCODE_OFFSET: u8 = 8; #[derive(Debug)] pub struct EventProcessor { - pub dnd: Dnd, pub ime_receiver: ImeReceiver, pub ime_event_receiver: ImeEventReceiver, pub randr_event_offset: u8, @@ -294,7 +296,10 @@ impl EventProcessor { unsafe { (self.target.xconn.xlib.XPending)(self.target.xconn.display) != 0 } } - pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool { + pub fn poll_one_event<'a>( + &mut self, + event_ptr: &'a mut MaybeUninit, + ) -> Option<&'a mut XEvent> { // This function is used to poll and remove a single event // from the Xlib event queue in a non-blocking, atomic way. // XCheckIfEvent is non-blocking and removes events from queue. @@ -304,20 +309,21 @@ impl EventProcessor { unsafe extern "C" fn predicate( _display: *mut XDisplay, _event: *mut XEvent, - _arg: *mut c_char, + _filter: *mut c_char, ) -> c_int { - // This predicate always returns "true" (1) to accept all events 1 } - unsafe { + let event_initialized = unsafe { (self.target.xconn.xlib.XCheckIfEvent)( self.target.xconn.display, - event_ptr, + event_ptr.as_mut_ptr(), Some(predicate), std::ptr::null_mut(), ) != 0 - } + }; + + event_initialized.then(|| unsafe { event_ptr.assume_init_mut() }) } pub fn init_device(&self, device: xinput::DeviceId) { @@ -423,21 +429,41 @@ impl EventProcessor { } if xev.message_type == atoms[XdndEnter] as c_ulong { - let source_window = xev.data.get_long(0) as xproto::Window; - let flags = xev.data.get_long(1); - let version = flags >> 24; - self.dnd.version = Some(version); - let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; - if !has_more_types { - let type_list = vec![ - xev.data.get_long(2) as xproto::Atom, - xev.data.get_long(3) as xproto::Atom, - xev.data.get_long(4) as xproto::Atom, - ]; - self.dnd.type_list = Some(type_list); - } else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } { - self.dnd.type_list = Some(more_types); - } + // Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event` + // never contending the lock. + let transfer_id = { + let mut dnd = self.target.dnd.borrow_mut(); + let source_window = xev.data.get_long(0) as xproto::Window; + let flags = xev.data.get_long(1); + + let version = flags >> 24; + + let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1; + let types: Vec<_> = if !has_more_types { + [ + xev.data.get_long(2) as xproto::Atom, + xev.data.get_long(3) as xproto::Atom, + xev.data.get_long(4) as xproto::Atom, + ] + .map(|ty_atom| SelectionType::new(atoms, ty_atom)) + .into_iter() + .collect() + } else if let Ok(more_types) = unsafe { dnd.get_type_list(source_window) } { + more_types + .into_iter() + .map(|ty_atom| SelectionType::new(atoms, ty_atom)) + .collect() + } else { + Default::default() + }; + + dnd.init_state(version, source_window, window, types.into()).transfer_id + }; + + app.window_event(&self.target, window_id, WindowEvent::DragEntered { + id: transfer_id, + position: None, + }); return; } @@ -464,121 +490,204 @@ impl EventProcessor { .xconn .translate_coords(self.target.root, window, x, y) .expect("Failed to translate window coordinates"); - self.dnd.position = PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64); - // By our own state flow, `version` should never be `None` at this point. - let version = self.dnd.version.unwrap_or(5); + // Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event` + // never contending the lock. + let transfer_id = { + let dnd = self.target.dnd.borrow(); + let Some(state) = dnd.state() else { + return; + }; + // By our own state flow, `state` should never be `None` at this point. + let version = state.version; - // Action is specified in versions 2 and up, though we don't need it anyway. - // let action = xev.data.get_long(4); + let time = if version == 0 { + // In version 0, time isn't specified + x11rb::CURRENT_TIME + } else { + xev.data.get_long(3) as xproto::Timestamp + }; - let accepted = if let Some(ref type_list) = self.dnd.type_list { - type_list.contains(&atoms[TextUriList]) - } else { - false - }; + // Log this timestamp. + self.target.xconn.set_timestamp(time); - if !accepted { unsafe { - self.dnd - .send_status(window, source_window, DndState::Rejected) - .expect("Failed to send `XdndStatus` message."); + dnd.send_status( + window, + source_window, + if state.accepted { DndState::Accepted } else { DndState::Rejected }, + ) + .expect("Failed to send `XdndStatus` message."); } - self.dnd.reset(); - return; - } - self.dnd.source_window = Some(source_window); - let time = if version == 0 { - // In version 0, time isn't specified - x11rb::CURRENT_TIME - } else { - xev.data.get_long(3) as xproto::Timestamp + state.transfer_id }; - // Log this timestamp. - self.target.xconn.set_timestamp(time); + app.window_event(&self.target, window_id, WindowEvent::DragPosition { + id: transfer_id, + position: PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64), + // `Copy` is the default. Other actions are possible in X11, but the specification + // does not properly explain how to implement them (only giving a vague description + // of `XdndMove`). For simplicity's sake, we simply do not implement non-copy drag + // on X11. + // See https://www.freedesktop.org/wiki/Specifications/XDND/ + proposed_action: Some(DndAction::Copy), + }); - // This results in the `SelectionNotify` event below - unsafe { - self.dnd.convert_selection(window, time); - } - - unsafe { - self.dnd - .send_status(window, source_window, DndState::Accepted) - .expect("Failed to send `XdndStatus` message."); - } return; } if xev.message_type == atoms[XdndDrop] as c_ulong { - let (source_window, state) = if let Some(source_window) = self.dnd.source_window { - if let Some(Ok(ref path_list)) = self.dnd.result { - let event = WindowEvent::DragDropped { - paths: path_list.iter().map(Into::into).collect(), - position: self.dnd.position, - }; - app.window_event(&self.target, window_id, event); - } - (source_window, DndState::Accepted) - } else { - // `source_window` won't be part of our DND state if we already rejected the drop in - // our `XdndPosition` handler. - let source_window = xev.data.get_long(0) as xproto::Window; - (source_window, DndState::Rejected) + let (source_window, transfer_id) = { + let dnd = self.target.dnd.borrow(); + let Some(state) = dnd.state() else { + warn!("Received `XdndDrop` without `XdndEnter`"); + return; + }; + let source_window = state.source_window; + + (source_window, state.transfer_id) }; - unsafe { - self.dnd - .send_finished(window, source_window, state) - .expect("Failed to send `XdndFinished` message."); + app.window_event( + &self.target, + window_id, + // TODO + WindowEvent::DragDropped { + id: transfer_id, + // `Copy` is the default. Other actions are possible in X11, but the + // specification does not properly explain how to implement + // them (only giving a vague description of `XdndMove`). For + // simplicity's sake, we simply do not implement non-copy drag + // on X11. + // See https://www.freedesktop.org/wiki/Specifications/XDND/ + proposed_action: Some(DndAction::Copy), + }, + ); + + let mut dnd = self.target.dnd.borrow_mut(); + + if let Some(state) = + dnd.state_mut().filter(|state| !state.pending_fetch_types.is_empty()) + { + state.finished = Some((window, source_window)); + } else { + unsafe { + dnd.send_finished(window, source_window) + .expect("Failed to send `XdndFinished` message."); + } } - self.dnd.reset(); return; } if xev.message_type == atoms[XdndLeave] as c_ulong { - if self.dnd.dragging { - let event = WindowEvent::DragLeft { position: Some(self.dnd.position) }; - app.window_event(&self.target, window_id, event); - } - self.dnd.reset(); + let dnd = self.target.dnd.borrow(); + let Some(state) = dnd.state() else { + return; + }; + app.window_event(&self.target, window_id, WindowEvent::DragLeft { + id: state.transfer_id, + }); } } fn selection_notify(&mut self, xev: &XSelectionEvent, app: &mut dyn ApplicationHandler) { let atoms = self.target.xconn.atoms(); - let window = xev.requestor as xproto::Window; - let window_id = mkwid(window); + let xwindow = xev.requestor as xproto::Window; // Set the timestamp. self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); + // For now, winit only supports selections for drag-and-drop. This should be changed + // when clipboard support is implemented. if xev.property != atoms[XdndSelection] as c_ulong { return; } - // This is where we receive data from drag and drop - self.dnd.result = None; - if let Ok(mut data) = unsafe { self.dnd.read_data(window) } { - let parse_result = self.dnd.parse_data(&mut data); + let (transfer_id, serial, type_) = { + let Some(state) = self.target.dnd.get_mut().state_mut() else { + return; + }; - if let Ok(ref path_list) = parse_result { - let event = if self.dnd.dragging { - WindowEvent::DragMoved { position: self.dnd.position } - } else { - let paths = path_list.iter().map(Into::into).collect(); - self.dnd.dragging = true; - WindowEvent::DragEntered { paths, position: self.dnd.position } + let Some((serial, type_)) = state.pending_fetch_types.pop_front() else { + return; + }; + + // Annoyingly, `xproto::Atom` and `x11_dl::Atom` are different on 64-bit + // but the same on 32-bit, so just casting to `u32` will cause "casting + // to same type" clippy warnings when compiled as 32-bit. + #[cfg(target_pointer_width = "32")] + let target_type = xev.target; + #[cfg(target_pointer_width = "64")] + let target_type = xev.target as u32; + + if target_type != type_.atom() { + let get_name = |atom| { + self.target + .xconn + .xcb_connection() + .get_atom_name(atom) + .ok() + .and_then(|cookie| cookie.reply().ok()) + .and_then(|reply| String::from_utf8(reply.name).ok()) }; - app.window_event(&self.target, window_id, event); + let expected_type_name = get_name(type_.atom()); + let found_type_name = get_name(xev.type_ as _); + + let extra_context = match (expected_type_name, found_type_name) { + (Some(expected), Some(found)) => { + format!(" (expected {expected}, found {found})") + }, + (Some(expected), None) => format!(" (expected {expected})"), + (None, Some(found)) => format!(" (found {found})"), + (None, None) => "".to_string(), + }; + warn!( + "Received `SelectionNotify` with unexpected type{extra_context}. Continuing, \ + but this may be a bug." + ); } - self.dnd.result = Some(parse_result); + (state.transfer_id, serial, type_) + }; + + let value = match self.target.dnd.borrow().read_data(xwindow, type_) { + Ok(value) => Arc::new(value), + Err(err) => { + warn!("Failed to read selection: {err}"); + return; + }, + }; + + let window_id = mkwid(xwindow); + + app.window_event(&self.target, window_id, WindowEvent::DataTransferReceived { + id: transfer_id, + serial, + value, + }); + + let dnd = self.target.dnd.borrow(); + + // If we have another fetch pending, request it from the drag source window + if let Some((window, type_)) = dnd.state().and_then(|state| { + state + .pending_fetch_types + .front() + .cloned() + .map(|(_, type_)| (state.target_window, type_)) + }) { + dnd.convert_selection(window, self.target.xconn.timestamp(), type_.atom()); + } else if let Some((this_window, target_window)) = + dnd.state().and_then(|state| state.finished) + { + unsafe { + dnd.send_finished(this_window, target_window) + .expect("Failed to send `XdndFinished` message."); + } } } diff --git a/winit-x11/src/lib.rs b/winit-x11/src/lib.rs index 41bb1382e..79c9d05e8 100644 --- a/winit-x11/src/lib.rs +++ b/winit-x11/src/lib.rs @@ -26,6 +26,8 @@ mod window; mod xdisplay; mod xsettings; +pub use dnd::{Selection, SelectionReader, SelectionType, UriListParseError}; + /// X window type. Maps directly to /// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html). #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] diff --git a/winit-x11/src/util/window_property.rs b/winit-x11/src/util/window_property.rs index 7329e7132..0f89a0d9d 100644 --- a/winit-x11/src/util/window_property.rs +++ b/winit-x11/src/util/window_property.rs @@ -17,6 +17,7 @@ pub enum GetPropertyError { X11rbError(Arc), TypeMismatch(xproto::Atom), FormatMismatch(c_int), + Unknown, } impl GetPropertyError { @@ -41,6 +42,7 @@ impl fmt::Display for GetPropertyError { GetPropertyError::X11rbError(err) => err.fmt(f), GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"), GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"), + GetPropertyError::Unknown => write!(f, "internal error"), } } } diff --git a/winit-x11/src/window.rs b/winit-x11/src/window.rs index 027773211..4c1a0f356 100644 --- a/winit-x11/src/window.rs +++ b/winit-x11/src/window.rs @@ -31,7 +31,14 @@ use x11rb::protocol::sync::{ConnectionExt as _, Int64}; use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle}; use x11rb::protocol::{randr, xinput}; -use crate::atoms::*; +use crate::atoms::{ + _GTK_THEME_VARIANT, _NET_ACTIVE_WINDOW, _NET_WM_ICON, _NET_WM_MOVERESIZE, _NET_WM_NAME, + _NET_WM_PID, _NET_WM_PING, _NET_WM_STATE, _NET_WM_STATE_ABOVE, _NET_WM_STATE_BELOW, + _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_HIDDEN, _NET_WM_STATE_MAXIMIZED_HORZ, + _NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_SYNC_REQUEST, _NET_WM_SYNC_REQUEST_COUNTER, + _NET_WM_WINDOW_TYPE, _XEMBED, AtomName, CARD32, UTF8_STRING, WM_CHANGE_STATE, + WM_CLIENT_MACHINE, WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, XdndAware, +}; use crate::event_loop::{ ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie, WakeSender, X11Error, xinput_fp1616_to_float, diff --git a/winit/build.rs b/winit/build.rs index 93ce11e4f..6376a099d 100644 --- a/winit/build.rs +++ b/winit/build.rs @@ -1,7 +1,14 @@ use cfg_aliases::cfg_aliases; -// Only relevant for examples and Winit, our usage of println! is fine here. -#[allow(clippy::disallowed_macros)] +#[allow( + clippy::disallowed_macros, + reason = "Only relevant for examples and Winit, our usage of println! is fine here." +)] +#[allow( + semicolon_in_expressions_from_macros, + reason = "This is a future incompatibility lint and we currently use cfg_aliases 0.2.1, which \ + has not been updated to resolve the latest lints" +)] fn main() { // Dummy invocation to enable change-tracking in build scripts. println!("cargo:rerun-if-changed=build.rs"); diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index 903034f92..b51c0cb14 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -1,10 +1,18 @@ use std::error::Error; +use std::path::PathBuf; +use std::sync::Arc; +use image::imageops::FilterType; +use image::{DynamicImage, GenericImageView, RgbImage}; use softbuffer::{Context, Surface}; -use tracing::info; +use tracing::{error, info, warn}; use winit::application::ApplicationHandler; -use winit::event::WindowEvent; -use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle}; +use winit::data_transfer::{DataTransferId, DataTransferSendBuilder, SendData, TypeHint}; +use winit::event::{ButtonSource, MouseButton, WindowEvent}; +use winit::event_loop::{ + ActiveEventLoop, AsyncRequestSerial, DndAction, DragIcon, EventLoop, OwnedDisplayHandle, +}; +use winit::icon::{Icon, RgbaIcon}; use winit::window::{Window, WindowAttributes, WindowId}; #[path = "util/fill.rs"] @@ -17,20 +25,55 @@ fn main() -> Result<(), Box> { let event_loop = EventLoop::new()?; - let app = Application::default(); + let app = Application::new(); Ok(event_loop.run_app(app)?) } /// Application state and event handling. -#[derive(Default, Debug)] +#[derive(Debug)] struct Application { surface: Option>>, + last_dnd_fetch: Option, + last_drag_start: Option, + drag_icon: (Icon, i32, i32), + drag_image_data: Arc, +} + +const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png"); + +impl Application { + fn new() -> Self { + let drag_icon = load_icon(DRAG_IMAGE); + let drag_image_data = Arc::new(image::load_from_memory(DRAG_IMAGE).unwrap().into_rgb8()); + Self { + surface: None, + last_dnd_fetch: None, + last_drag_start: None, + drag_icon, + drag_image_data, + } + } +} + +fn load_icon(bytes: &[u8]) -> (Icon, i32, i32) { + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(bytes).unwrap().into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + ( + RgbaIcon::new(icon_rgba, icon_width, icon_height).expect("Failed to open icon").into(), + -(icon_width as i32) / 2, + -(icon_height as i32) / 2, + ) } impl ApplicationHandler for Application { fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) { let window_attributes = - WindowAttributes::default().with_title("Drag and drop files on me!"); + WindowAttributes::default().with_title("Drag and drop files, text or HTML onto me!"); + let window = event_loop.create_window(window_attributes).unwrap(); let context = Context::new(event_loop.owned_display_handle()).unwrap(); let surface = Surface::new(&context, window).unwrap(); @@ -40,14 +83,168 @@ impl ApplicationHandler for Application { fn window_event( &mut self, event_loop: &dyn ActiveEventLoop, - _window_id: WindowId, + window_id: WindowId, event: WindowEvent, ) { match event { - WindowEvent::DragLeft { .. } - | WindowEvent::DragEntered { .. } - | WindowEvent::DragMoved { .. } - | WindowEvent::DragDropped { .. } => { + WindowEvent::PointerButton { button: ButtonSource::Mouse(button), state, .. } + if button == MouseButton::Left && state.is_pressed() => + { + let (icon, offset_x, offset_y) = self.drag_icon.clone(); + + // In a real application, you probably wouldn't advertise so many types. + // Depending on platform and destination application, different options may be + // chosen. + let result = event_loop.start_drag( + window_id, + DataTransferSendBuilder::new(self.drag_image_data.clone()) + .with_type(TypeHint::UriList, |_, _| { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root = manifest_dir.parent().unwrap(); + let this_file = root.join(file!()); + let icon_file = this_file.parent().unwrap().join("data/icon.png"); + + SendData::from_file_paths([icon_file]) + }) + .with_type(TypeHint::Plaintext, |_, _| Some("Winit example".to_string())) + .with_type(TypeHint::Html, |_, _| { + Some("Winit example".to_string()) + }) + // You can advertise a `TypeHint` that can match many types, and switch + // inside the callback. For example, this will match any image type. + // This may be desirable on some platforms which restrict the set of + // image types that can be sent. + .with_type(TypeHint::Image { extension_hint: None }, |image, ty| { + let hint = ty.hint()?; + match hint { + TypeHint::Image { extension_hint } => { + let image = DynamicImage::from((**image).clone()); + let (w, h) = image.dimensions(); + let image = image + .resize(w * 8, h * 8, FilterType::Gaussian) + .into_rgb8(); + let ext = extension_hint.unwrap_or("png"); + info!("Destination requested image as {ext}, converting..."); + let format = image::ImageFormat::from_extension(ext)?; + let mut out_buf = Vec::new(); + let mut out_writer = std::io::Cursor::new(&mut out_buf); + + image.write_to(&mut out_writer, format).ok()?; + + Some(out_buf) + }, + _ => None, + } + }) + .build(), + &[DndAction::Move, DndAction::Copy], + Some(DragIcon { icon, offset_x, offset_y }), + ); + + self.last_drag_start = result.ok(); + }, + WindowEvent::DragLeft { .. } => { + info!("{event:?}"); + }, + WindowEvent::DataTransferReceived { ref value, serial, .. } => { + assert_eq!(self.last_dnd_fetch, Some(serial)); + + match value.type_().hint() { + Some(TypeHint::Plaintext | TypeHint::Html) => { + let Ok(text) = value.try_as_string() else { + return; + }; + info!("{text:?}"); + }, + Some(TypeHint::UriList) => { + let Ok(uris) = value.try_as_uris() else { + return; + }; + info!("URIs: {uris:#?}"); + + // If you only want to support dropping files, rather than arbitrary URIs, + // you can use the `try_as_file_paths` helper method. + let Ok(uris_as_paths) = value.try_as_file_paths() else { + return; + }; + info!("URIs as file paths: {uris_as_paths:#?}"); + }, + Some(TypeHint::Image { extension_hint: ext }) => { + let Ok(bytes) = value.try_as_bytes() else { + return; + }; + let format = ext.and_then(image::ImageFormat::from_extension); + + let reader = std::io::Cursor::new(&bytes[..]); + let reader = match format { + Some(fmt) => image::ImageReader::with_format(reader, fmt), + None => image::ImageReader::new(reader), + }; + + match reader.decode() { + Ok(image) => { + let width = image.width(); + let height = image.height(); + info!("Received image ({width}x{height})"); + }, + Err(err) => { + warn!("Failed to decode image: {err}"); + }, + } + }, + _ => { + unreachable!("Received a type we didn't ask for!"); + }, + } + }, + WindowEvent::DragPosition { .. } => { + info!("{event:?}"); + }, + WindowEvent::DragDropped { .. } => { + info!("{event:?}"); + }, + WindowEvent::DragEntered { id, .. } => { + info!("{event:?}"); + + let data_transfer = match event_loop.data_transfer(id) { + Ok(dt) => dt, + Err(e) => { + error!("{e}"); + return; + }, + }; + + info!("Types: {:#?}", data_transfer.available_types()); + + let readable_image_types = image::ImageFormat::all() + .filter(|fmt| fmt.reading_enabled()) + .filter_map(|fmt| { + let ext = fmt.extensions_str().first()?; + + Some(TypeHint::Image { extension_hint: Some(ext) }) + }); + + let mut valid_types = readable_image_types.chain([ + TypeHint::Html, + TypeHint::UriList, + TypeHint::Plaintext, + ]); + + let valid_type = valid_types.find(|ty| data_transfer.has_type(ty)); + + let Some(type_) = valid_type else { + event_loop.set_valid_dnd_actions(id, &[]).unwrap(); + return; + }; + + event_loop.set_valid_dnd_actions(id, &[DndAction::Move, DndAction::Copy]).unwrap(); + + self.last_dnd_fetch = event_loop.fetch_data_transfer(id, &type_).ok(); + }, + WindowEvent::OutgoingDragDropped { .. } => { + info!("{event:?}"); + }, + WindowEvent::OutgoingDragCanceled { .. } => { info!("{event:?}"); }, WindowEvent::RedrawRequested => { diff --git a/winit/src/lib.rs b/winit/src/lib.rs index beb5c809e..bab3fd990 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -292,7 +292,9 @@ pub use rwh_06 as raw_window_handle; #[cfg(any(doc, doctest, test))] pub mod changelog; pub mod event_loop; -pub use winit_core::{application, cursor, error, event, icon, keyboard, monitor, window}; +pub use winit_core::{ + application, cursor, data_transfer, error, event, icon, keyboard, monitor, window, +}; #[macro_use] mod os_error; mod platform_impl;