diff --git a/Cargo.toml b/Cargo.toml index 3321df958d..eee26d23bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ cursor-icon = "1.1.0" dpi = { version = "0.1.2", path = "dpi" } keyboard-types = "0.8.0" mint = "0.5.6" +rustversion = "1.0" rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"] } serde = { version = "1", features = ["serde_derive"] } smol_str = "0.3" diff --git a/typos.toml b/typos.toml index c454f23b9b..20ebeff8c3 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 1094510621..54b366a29d 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 5b057b79b3..c20054defd 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,11 +7,13 @@ use std::time::Instant; use dispatch2::MainThreadBound; use objc2::MainThreadMarker; +use objc2::rc::{Retained, Weak}; use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, 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::window::WindowId; @@ -18,10 +21,14 @@ 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::{DragOperation, Pasteboards}; +use crate::window_delegate::WindowDelegate; #[derive(Debug)] pub(super) struct AppState { mtm: MainThreadMarker, + drag_state: Cell>, + pasteboards: Pasteboards, activation_policy: Option, default_menu: bool, activate_ignoring_other_apps: bool, @@ -43,10 +50,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(Copy, Clone, Debug)] +pub(crate) struct DragState { + pub id: DataTransferId, + pub valid_operations: DragOperation, +} + // SAFETY: Creating `MainThreadBound` in a `const` context, where there is no concept of the // main thread. static GLOBAL: MainThreadBound>> = @@ -65,6 +79,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 +98,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 +113,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 new_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 +402,14 @@ 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) -> &Cell> { + &self.drag_state + } } /// Returns the minimum `Option`, taking into account that `None` diff --git a/winit-appkit/src/cursor.rs b/winit-appkit/src/cursor.rs index e6e3337fff..0902371467 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!("parent view should be installed in a window"))?; + + 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 0000000000..aa0ce33797 --- /dev/null +++ b/winit-appkit/src/dnd.rs @@ -0,0 +1,519 @@ +use std::cell::{OnceCell, RefCell}; +use std::collections::HashMap; +use std::ffi::OsString; +use std::io; +use std::ops::{ControlFlow, Deref}; +use std::rc::Rc; + +use objc2::rc::{Retained, Weak}; +use objc2::runtime::AnyObject; +use objc2::{AnyThread, DefinedClass as _, 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::{DndActionMask, DndActions}; + +/// 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: Retained, +} + +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.retain() }) + }) + } +} + +impl Deref for PasteboardType { + type Target = Retained; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +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 } + } +} + +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(Clone, Debug)] +pub struct Pasteboard> { + transfer_id: DataTransferId, + inner: PB, + types: OnceCell>, +} + +impl Deref for Pasteboard { + type Target = Retained; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl Pasteboard { + fn new(transfer_id: DataTransferId, pasteboard: Retained) -> Self { + Self { transfer_id, inner: 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.inner + .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, dyn_type: &dyn TransferType) -> Option { + if self.has_type(dyn_type) { + Some(PasteboardValue { + type_: PasteboardTypeSpec::from_dyn(dyn_type)?, + inner: self.clone(), + }) + } else { + None + } + } +} + +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)] +enum PasteboardTypeSpec { + PasteboardType(PasteboardType), + TypeHint(TypeHint), +} + +impl PasteboardTypeSpec { + 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, + } + } +} + +/// A thin wrapper around [`NSDragOperation`], implementing [`DndActionMask`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct DragOperation(pub NSDragOperation); + +impl DragOperation { + /// An empty set of drag operations + pub fn empty() -> Self { + Self(NSDragOperation::empty()) + } + + pub(crate) fn from_dyn(actions: &dyn DndActionMask) -> Self { + if let Some(op) = actions.cast_ref::() { + *op + } else { + match actions.hint() { + DndActions::Flags { move_, copy, link } => { + let move_flag = + if move_ { NSDragOperation::Move } else { NSDragOperation::empty() }; + let copy_flag = + if copy { NSDragOperation::Copy } else { NSDragOperation::empty() }; + let link_flag = + if link { NSDragOperation::Link } else { NSDragOperation::empty() }; + Self(move_flag | copy_flag | link_flag) + }, + DndActions::All => Self(NSDragOperation::all()), + } + } + } + + fn intersection(&self, other: &Self) -> Self { + Self(self.0.intersection(other.0)) + } + + fn intersects(&self, other: &Self) -> bool { + self.0.intersects(other.0) + } +} + +impl DndActionMask for DragOperation { + fn hint(&self) -> DndActions { + if self.0.is_all() { + DndActions::All + } else { + DndActions::Flags { + move_: self.0.contains(NSDragOperation::Move), + copy: self.0.contains(NSDragOperation::Copy), + link: self.0.contains(NSDragOperation::Link), + } + } + } + + fn intersection(&self, other: &dyn DndActionMask) -> Box { + Box::new(self.intersection(&Self::from_dyn(other))) + } + + fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn intersects(&self, other: &dyn DndActionMask) -> bool { + self.intersects(&Self::from_dyn(other)) + } +} + +/// A thin wrapper around [`NSPasteboard`], implementing [`TypedValue`]. +#[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, + inner: Pasteboard, +} + +impl PasteboardValue { + fn single_file_url(&self) -> Option { + self.inner + .stringForType(unsafe { NSPasteboardTypeFileURL }) + .map(|ns_str| ns_str.to_string()) + } +} + +impl Deref for PasteboardValue { + type Target = Retained; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl TypedData for PasteboardValue { + fn type_(&self) -> &dyn TransferType { + match &self.type_ { + PasteboardTypeSpec::PasteboardType(pasteboard_type) => pasteboard_type, + PasteboardTypeSpec::TypeHint(type_hint) => type_hint, + } + } + + fn try_read(&mut self) -> Option> { + struct DataReader { + inner: Retained, + offset: usize, + } + + impl DataReader { + fn new(data: Retained) -> Self { + Self { inner: data, offset: 0 } + } + } + + impl io::Read for DataReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let end = (self.offset + buf.len()).min(self.inner.len()); + let range = self.offset..end; + self.offset = end; + let bytes = unsafe { self.inner.as_bytes_unchecked() }; + let src = &bytes[range]; + buf[..src.len()].copy_from_slice(src); + + Ok(src.len()) + } + } + + impl io::BufRead for DataReader { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + Ok(unsafe { self.inner.as_bytes_unchecked() } + .get(self.offset..) + .unwrap_or_default()) + } + + fn consume(&mut self, amount: usize) { + self.offset = (self.offset + amount).min(self.inner.len()) + } + } + + self.inner + .dataForType(self.type_.pasteboard_type()?) + .map(|data| Box::new(DataReader::new(data)) as _) + } + + fn try_as_uris(&mut 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()); + } + + let Some(items) = self.inner.pasteboardItems() else { + // The pasteboard didn't expose any items, so we try with the deprecated method. + #[expect(deprecated)] + let property_list = match self + .inner + .propertyListForType(unsafe { objc2_app_kit::NSFilenamesPboardType }) + { + Some(property_list) => property_list, + None => { + return self + .single_file_url() + .map(|str| vec![str.into()]) + .ok_or_else(|| io::ErrorKind::InvalidData.into()); + }, + }; + + let paths = property_list + .downcast::() + .unwrap() + .into_iter() + .map(|file| file.downcast::().unwrap().to_string().into()) + .collect(); + + return Ok(paths); + }; + + Ok(items + .into_iter() + .filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL })) + .map(|ns_str| ns_str.to_string().into()) + .collect()) + } + + fn try_as_string(&mut self) -> io::Result { + self.inner + .stringForType(self.type_.pasteboard_type().ok_or(io::ErrorKind::InvalidData)?) + .map(|ns_str| ns_str.to_string()) + .ok_or_else(|| io::ErrorKind::InvalidData.into()) + } +} + +#[derive(Debug, Default)] +pub struct Pasteboards { + inner: RefCell>>, +} + +impl Pasteboards { + pub fn remove_deloaded_pasteboards(&self) { + self.inner.borrow_mut().retain(|_, v| v.load().is_some()); + } + + /// If the data transfer exists, update the pasteboard it points to. + pub fn set_pasteboard(&self, id: DataTransferId, pb: &Retained) { + let mut inner = self.inner.borrow_mut(); + if let Some(state) = inner.get_mut(&id) { + *state = Weak::from_retained(pb); + } + } + + pub fn insert(&self, transfer_id: DataTransferId, pb: &Retained) { + self.inner.borrow_mut().insert(transfer_id, Weak::from_retained(pb)); + } + + pub fn get(&self, id: DataTransferId) -> Option { + self.inner.borrow().get(&id).and_then(|weak| weak.load()).map(|pb| Pasteboard::new(id, pb)) + } +} + +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((**pb_type).clone()); + + 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; + // TODO: Not necessarily ideal to always use `Promised`, but + // it's good enough for now. + 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 881762075e..e46307a0f1 100644 --- a/winit-appkit/src/event_loop.rs +++ b/winit-appkit/src/event_loop.rs @@ -1,30 +1,37 @@ +use std::ffi::OsString; +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, TypedData, +}; use winit_core::error::{EventLoopError, RequestError}; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, 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 +39,9 @@ use super::cursor::CustomCursor; use super::event::dummy_event; use super::monitor; use crate::ActivationPolicy; +use crate::app_state::DragState; +use crate::cursor::image_from_icon; +use crate::dnd::{DragOperation, PasteboardWriter}; use crate::window::Window; #[derive(Debug)] @@ -126,8 +136,163 @@ 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, RequestError> { + let Some(pb) = self.app_state.pasteboards().get(id) else { + return Err(RequestError::Ignored); + }; + + pb.with_type(type_).map(|value| Box::new(value) as _).ok_or(RequestError::Ignored) + } + + 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_actions( + &self, + id: DataTransferId, + actions: &dyn DndActionMask, + ) -> Result<(), RequestError> { + let Some(drag_state) = self.app_state.drag_state().get() else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + + if drag_state.id != id { + return Err(os_error!(UnknownDataTransfer(id)).into()); + } + let new_drag_state = DragState { id, valid_operations: DragOperation::from_dyn(actions) }; + + self.app_state.drag_state().set(Some(new_drag_state)); + + Ok(()) + } + + fn start_drag( + &self, + source: WindowId, + send_data: Box, + action_mask: &dyn DndActionMask, + icon: Option, + ) -> Result { + let drag_operation = DragOperation::from_dyn(action_mask); + + self.app_state + .with_window_delegate_on_main(source, move |delegate| { + let dragging_rect_offset = + icon.as_ref().map(|icon| icon.offset).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, + event_location.y + 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_os_str = + |os_str: OsString| Some(NSString::from_str(os_str.to_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().filter_map(ns_url_from_os_str)), + ), + SendData::String(string) => Some( + Some(NSString::from_str(&string)) + .into_iter() + .chain(Vec::new().into_iter().filter_map(ns_url_from_os_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 bcec63f14b..8548f8f738 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 818e460277..e1bb425c56 100644 --- a/winit-appkit/src/view.rs +++ b/winit-appkit/src/view.rs @@ -8,8 +8,8 @@ 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, NSWindow, + NSApplication, NSCursor, NSDraggingSession, NSEvent, NSEventPhase, NSResponder, + NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow, }; use objc2_core_foundation::CGRect; use objc2_foundation::{ @@ -32,6 +32,7 @@ use super::event::{ }; use super::window::window_id; use crate::OptionAsAlt; +use crate::dnd::DragOperation; #[derive(Debug)] struct CursorState { @@ -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, @@ -780,6 +787,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(DragOperation::empty()), cursor_state: Default::default(), ime_position: Default::default(), ime_size: Default::default(), @@ -854,7 +863,7 @@ impl WinitView { }); } - fn scale_factor(&self) -> f64 { + pub(crate) fn scale_factor(&self) -> f64 { self.window().backingScaleFactor() as f64 } @@ -1073,6 +1082,29 @@ impl WinitView { self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get())); } + pub(crate) fn set_dragging_session( + &self, + drag: Retained, + action_mask: DragOperation, + ) { + let vars = self.ivars(); + vars.dragging_session.replace(Some(drag)); + vars.drag_operations.set(action_mask); + } + + pub(crate) fn drag_operations(&self) -> DragOperation { + self.ivars().drag_operations.get() + } + + pub(crate) fn clear_dragging_session(&self, drag: &NSDraggingSession) -> bool { + let vars = self.ivars(); + vars.drag_operations.set(DragOperation::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 bc6cde65df..24370d5196 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.new_window(&delegate, mtm); Ok(Window { window: MainThreadBound::new(delegate.window().retain(), mtm), delegate: MainThreadBound::new(delegate, mtm), @@ -339,6 +340,8 @@ impl CoreWindow for Window { fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle { self } + + // TODO: `set_valid_actions` } define_class!( diff --git a/winit-appkit/src/window_delegate.rs b/winit-appkit/src/window_delegate.rs index e316973e7c..4c1ac6a8d9 100644 --- a/winit-appkit/src/window_delegate.rs +++ b/winit-appkit/src/window_delegate.rs @@ -19,14 +19,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 +44,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 +61,8 @@ 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::DragOperation; use crate::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS}; #[derive(Debug)] @@ -358,37 +361,58 @@ 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().0 + } + + #[unsafe(method(draggingSession:endedAtPoint:operation:))] + fn dragging_session_ended_at_point( + &self, + session: &NSDraggingSession, + _: NSPoint, + _: NSDragOperation, + ) { + 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 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 vars = self.ivars(); - true + let transfer_id = DataTransferId::from_raw(sender.draggingSequenceNumber() as i64); + vars.app_state.pasteboards().insert(transfer_id, &pb); + let valid_operations = NSDragOperation::empty(); + vars.app_state.drag_state().set(Some(DragState { + id: transfer_id, + valid_operations: DragOperation(valid_operations), + })); + + self.queue_event(WindowEvent::DragEntered { + id: transfer_id, + position: Some(position), + }); + + vars.app_state + .drag_state() + .get() + .map_or(NSDragOperation::empty(), |state| state.valid_operations.0) } #[unsafe(method(wantsPeriodicDraggingUpdates))] @@ -400,17 +424,30 @@ 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(DragState { id: transfer_id, .. }) = vars.app_state.drag_state().get() else { + return NSDragOperation::empty(); + }; + + let pb = sender.draggingPasteboard(); + + 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 }); + self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); - true + vars.app_state + .drag_state() + .get() + .map_or(NSDragOperation::empty(), |state| state.valid_operations.0) } /// Invoked when the image is released @@ -425,37 +462,41 @@ define_class!( fn perform_drag_operation(&self, sender: &ProtocolObject) -> bool { let _entered = debug_span!("performDragOperation:").entered(); - use std::path::PathBuf; - - let pb = sender.draggingPasteboard(); + let vars = self.ivars(); - #[allow(deprecated)] - let property_list = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) { - Some(property_list) => property_list, - None => return false.into(), + let Some(DragState { id: transfer_id, .. }) = vars.app_state.drag_state().get() else { + return false.into(); }; - let paths = property_list - .downcast::() - .unwrap() - .into_iter() - .map(|file| PathBuf::from(file.downcast::().unwrap().to_string())) - .collect(); + let pb = sender.draggingPasteboard(); + + 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 }); + self.queue_event(WindowEvent::DragPosition { id: transfer_id, position }); + self.queue_event(WindowEvent::DragDropped { id: transfer_id }); - true + let valid_operations = vars + .app_state + .drag_state() + .get() + .map_or(NSDragOperation::empty(), |state| state.valid_operations.0); + + sender.draggingSourceOperationMask().intersects(valid_operations) } /// Invoked when the dragging operation is complete #[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().set(None); } /// Invoked when the dragging operation is cancelled @@ -463,13 +504,26 @@ 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(DragState { id: transfer_id, .. }) = vars.app_state.drag_state().get() else { + return; + }; + + if let Some(sender) = sender { + let pb = sender.draggingPasteboard(); + 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::DragPosition { id: transfer_id, position }); + } - self.queue_event(WindowEvent::DragLeft { position }); + self.queue_event(WindowEvent::DragLeft { id: transfer_id }); + + vars.app_state.drag_state().set(None); } } @@ -674,7 +728,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 +810,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 +887,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/src/data_transfer.rs b/winit-core/src/data_transfer.rs new file mode 100644 index 0000000000..1e0dfbab5d --- /dev/null +++ b/winit-core/src/data_transfer.rs @@ -0,0 +1,446 @@ +//! Types related to data transfer, used for clipboard and drag-and-drop. +//! +//! This module contains types and traits implementing cross-application data transfer. +//! While the precise implementation depends on platform, there are a set of types which +//! can be safely transferred between applications on all platforms (see [`TypeHint`]). +//! +//! On all platforms, the process looks something like this: +//! +//! - A data transfer advertises a set of types which the data can be interpreted as +//! - 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 [`DragEnter`](crate::event::WindowEvent::DragEnter) 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). +//! - As the drag operation continues, the window will receive +//! [`DragMoved`](crate::event::WindowEvent::DragEnter) events. +//! - While `DragMoved` events are being received, the receiving application may mark the data as +//! being accepted or rejected. This will update the OS/compositor to display the correct UI to +//! the user. Accepting does not "finalize" the drag operation, nor does rejecting cancel it. +//! - 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 a +//! [`DragDropped`](crate::event::WindowEvent::DragDropped) event. In general, the receiving +//! application should assume that calling [`reject_drag`](crate::window::Window::reject_drag) +//! after `DragDropped` is received ends the lifecycle of the data transfer. +//! +//! If platform-dependent behavior is required, a platform may define internal types +//! implementing the traits in this module, which can then be accessed in an application +//! using the methods defined on [`dyn AsAny`]. See each platform's documentation for details. + +#![warn(missing_docs)] + +use std::ffi::OsString; +use std::marker::PhantomData; +use std::ops::ControlFlow; +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 audio 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); + +/// 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 { + /// 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. + fn try_read(&mut self) -> Option>; + + /// Read this value as a list of URIs. + /// + /// If this value is not readable as URIs, return `None`. + /// + /// The format of the returned URIs is simply a vector of strings. No validation is done + /// to ensure that the URIs are valid or in the format + fn try_as_uris(&mut self) -> io::Result>; + + /// Read this value as a plain text string. + /// + /// If this value is not readable as a string, return `None`. + fn try_as_string(&mut self) -> io::Result; +} + +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 +/// [`Window::fetch_data_transfer`](crate::window::Window::fetch_data_transfer) +/// and [`WindowEvent::DataTransferResult`](crate::event::WindowEvent::DataTransferResult). +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. +pub enum SendData { + /// File URIs + Uris(Vec), + /// String + String(String), + /// Binary blob + Bytes(Vec), +} + +impl From for SendData { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl From> for SendData { + fn from(value: Vec) -> Self { + Self::Bytes(value) + } +} + +// 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: Vec) -> Self { + Self::Uris(value) + } +} + +/// Trait for sending data via a data transfer. +/// +/// See [`StartDrag`](crate::event_loop::StartDrag) 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; + + /// If `true`, this data transfer is only valid for the application sending the data. + /// + /// This is useful on Wayland and macOS, which allow expressing internal drag-and-drop in the + /// API. On platforms which make no distinction between internal and external drag-and-drop, + /// this is ignored. + fn is_internal_only(&self) -> bool; +} + +impl_dyn_casting!(DataTransferSend); + +/// Marker for a [`DataTransferSendBuilder`] which is internal-only. +pub enum InternalTransferMarker {} +/// Marker for a [`DataTransferSendBuilder`] which is external. +pub enum ExternalTransferMarker {} + +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)>, + _is_internal: PhantomData, +} + +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 + M: 'static, + 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_) + } + + fn is_internal_only(&self) -> bool { + false + } +} + +impl DataTransferSend for DataTransferSendBuilder +where + T: fmt::Debug + Send + 'static, +{ + fn data_for_type(&self, type_: &dyn TransferType) -> Option { + self.data_for_type(type_) + } + + fn is_internal_only(&self) -> bool { + true + } +} + +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![], _is_internal: PhantomData } + } +} + +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_internal(state: T) -> Self { + Self { state, types: vec![], _is_internal: PhantomData } + } +} + +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 + 'static, + Self: DataTransferSend, +{ + /// 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 e6500c2133..e1bbafa461 100644 --- a/winit-core/src/event.rs +++ b/winit-core/src/event.rs @@ -2,7 +2,6 @@ use std::cell::LazyCell; use std::cmp::Ordering; use std::f64; -use std::path::PathBuf; use std::sync::{Mutex, Weak}; use dpi::{PhysicalPosition, PhysicalSize}; @@ -11,6 +10,7 @@ use serde::{Deserialize, Serialize}; use smol_str::SmolStr; use crate::Instant; +use crate::data_transfer::DataTransferId; use crate::error::RequestError; use crate::event_loop::AsyncRequestSerial; use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState}; @@ -77,39 +77,38 @@ pub enum WindowEvent { /// A file drag operation has entered the window. 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, + /// Data transfer object specifying the ID and available types. + 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). + /// + /// Some platforms will provide this on enter, others do not. On most platforms, the + /// receiving application is expected to either accept or reject a drag-and-drop operation + /// immediately upon receiving `DragEntered`, so when possible it is provided here to make + /// that behavior easier to implement correctly. + position: Option>, }, /// 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). + DragPosition { + /// Data transfer object specifying the ID and available types. + 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 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, + /// Data transfer object specifying the ID and available types. + id: DataTransferId, }, /// 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). - /// - /// ## Platform-specific - /// - /// - **Windows:** Always emits [`None`]. - position: Option>, + /// Data transfer object specifying the ID and available types. + id: DataTransferId, }, /// The window gained or lost focus. @@ -1560,16 +1559,19 @@ mod tests { use crate::event::Ime::Enabled; use crate::event::WindowEvent::*; use crate::event::{PointerKind, PointerSource}; + 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() }); + with_window_event(DragDropped { id: dnd_data }); + 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 3ef3ed5f59..da5a5cfb38 100644 --- a/winit-core/src/event_loop/mod.rs +++ b/winit-core/src/event_loop/mod.rs @@ -8,14 +8,19 @@ use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; +use dpi::PhysicalPosition; 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, TypedData, +}; +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 +119,89 @@ 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. + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result, RequestError> { + 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 `DndActionMask` as the valid actions for the given [`DataTransferId`], + /// presuming that the transfer ID is from a drag-and-drop operation. + /// + /// This allows the OS/compositor to display the correct UI, indicating that the dragged data + /// can be dropped. + fn set_valid_actions( + &self, + id: DataTransferId, + actions: &dyn DndActionMask, + ) -> 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::DataTransferSend) for how to create a + /// new cross-platform data transfer, or see the platform-specific implementation of + /// [`DataTransferSend`]. + /// + /// ### Arguments + /// + /// - `send_data` - The data provided by this drag operation. See + /// [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSendBuilder). + /// - `action_mask` - The set of valid actions for this drag operation. See + /// [`DndActions`](crate::data_transfer::DndActions). + /// - `icon` - 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`. + fn start_drag( + &self, + source: WindowId, + send_data: Box, + action_mask: &dyn DndActionMask, + icon: Option, + ) -> Result { + let _ = source; + let _ = send_data; + let _ = action_mask; + 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 +210,145 @@ 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 offset applied to the dragged icon. 0,0 means that the top-left point + /// of the icon will be at the cursor. + pub offset: PhysicalPosition, +} + +impl From for DragIcon { + fn from(value: Icon) -> Self { + Self { icon: value, offset: Default::default() } + } +} + +/// A mask of valid actions for a drag and drop operation. +/// +/// Inspired by [the `dropEffect` DOM API](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/dropEffect). +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum DndActions { + /// A specific set of operations. + /// + /// This is limited to the same set of cross-platform actions supported by `dropEffect`. + Flags { + /// Move the dragged item from the source to the destination. + move_: bool, + /// Copy the dragged item from the source to the destination. + copy: bool, + /// A link is established between the source and the destination. + link: bool, + }, + /// All actions, including platform-specific ones not represented in `Self::Flags`. + /// + /// Most commonly used as a default mask, in case the user just wants to support everything. + All, +} + +impl DndActions { + pub const fn new_copy() -> Self { + Self::Flags { move_: false, copy: true, link: false } + } + + pub const fn new_move() -> Self { + Self::Flags { move_: true, copy: false, link: false } + } + + pub const fn new_link() -> Self { + Self::Flags { move_: false, copy: false, link: true } + } + + pub const fn copy(&self) -> bool { + match *self { + DndActions::Flags { copy, .. } => copy, + DndActions::All => true, + } + } + + pub const fn move_(&self) -> bool { + match *self { + DndActions::Flags { move_, .. } => move_, + DndActions::All => true, + } + } + + pub const fn link(&self) -> bool { + match *self { + DndActions::Flags { link, .. } => link, + DndActions::All => true, + } + } + + pub const fn all() -> Self { + Self::All + } + + pub const fn none() -> Self { + Self::Flags { move_: false, copy: false, link: false } + } + + pub const fn any(&self) -> bool { + match *self { + Self::All => true, + Self::Flags { move_, copy, link } => move_ || copy || link, + } + } + + pub const fn is_empty(&self) -> bool { + !self.any() + } + + pub const fn intersects(&self, other: &Self) -> bool { + self.intersection(other).any() + } + + pub const fn intersection(&self, other: &Self) -> Self { + match (*self, *other) { + (Self::All, other) | (other, Self::All) => other, + ( + Self::Flags { move_: this_move, copy: this_copy, link: this_link }, + Self::Flags { move_: other_move, copy: other_copy, link: other_link }, + ) => Self::Flags { + move_: this_move && other_move, + copy: this_copy && other_copy, + link: this_link && other_link, + }, + } + } +} + +pub trait DndActionMask: AsAny + fmt::Debug { + fn hint(&self) -> DndActions; + fn intersection(&self, other: &dyn DndActionMask) -> Box; + fn is_empty(&self) -> bool; + + fn intersects(&self, other: &dyn DndActionMask) -> bool { + !self.intersection(other).is_empty() + } +} + +impl_dyn_casting!(DndActionMask); + +impl DndActionMask for DndActions { + fn hint(&self) -> DndActions { + *self + } + + fn intersects(&self, other: &dyn DndActionMask) -> bool { + self.intersects(&other.hint()) + } + + fn intersection(&self, other: &dyn DndActionMask) -> Box { + Box::new(self.intersection(&other.hint())) + } + + fn is_empty(&self) -> bool { + self.is_empty() + } +} + /// 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 89bc682164..2fe5fc82ee 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-core/src/window.rs b/winit-core/src/window.rs index 5355369acc..35e604ed2f 100644 --- a/winit-core/src/window.rs +++ b/winit-core/src/window.rs @@ -42,7 +42,8 @@ impl WindowId { impl fmt::Debug for WindowId { fn fmt(&self, fmtr: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(fmtr) + let inner = self.0; + write!(fmtr, "{inner:?}") } } diff --git a/winit-wayland/src/data_device.rs b/winit-wayland/src/data_device.rs new file mode 100644 index 0000000000..becfda56ea --- /dev/null +++ b/winit-wayland/src/data_device.rs @@ -0,0 +1,46 @@ +impl DataOfferHandler for WinitState { + fn source_actions( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut DragOffer, + actions: DndAction, + ) { + let seat = offer.winit_data().seat(); + let seat_state = match self.seats.get(&seat.id()) { + Some(seat_state) => seat_state, + None => { + warn!("Received pointer event without seat"); + return; + }, + }; + + let themed_pointer = match seat_state.pointer.as_ref() { + Some(pointer) => pointer, + None => { + warn!("Received pointer event without pointer"); + return; + }, + }; + + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + todo!() + } + + fn selected_action( + &mut self, + conn: &Connection, + qh: &QueueHandle, + offer: &mut sctk::data_device_manager::data_offer::DragOffer, + actions: wayland_client::protocol::wl_data_device_manager::DndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + todo!() + } +} diff --git a/winit-wayland/src/dnd.rs b/winit-wayland/src/dnd.rs new file mode 100644 index 0000000000..a7d7124def --- /dev/null +++ b/winit-wayland/src/dnd.rs @@ -0,0 +1,798 @@ +//! Types related to drag-and-drop and data transfer on Wayland. + +#![warn(missing_docs)] + +use std::ffi::OsString; +use std::fmt; +use std::fs::File; +use std::io::{self, BufRead, BufReader, Read, Write}; +use std::ops::Deref; +use std::os::fd::OwnedFd; +use std::sync::Arc; + +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 wayland_client::protocol::wl_data_device::WlDataDevice; +use wayland_client::protocol::wl_data_device_manager::DndAction; +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::{DndActionMask, DndActions}; +use winit_core::window::WindowId; + +use crate::make_data_transfer_id; +use crate::state::WinitState; + +impl DataSourceHandler for WinitState { + fn accept_mime( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &WlDataSource, + mime: Option, + ) { + let _ = mime; + let _ = source; + let _ = qh; + let _ = conn; + // This is unnecessary. + } + + fn send_request( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlDataSource, + mime: String, + mut 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; + }; + + match send_data { + SendData::Uris(os_strings) => { + let mut iter = os_strings.into_iter(); + let Some(first) = iter.next() else { + return; + }; + + if fd.write_all(first.as_encoded_bytes()).is_err() { + return; + } + + // TODO: Is there something better we can do than unconditionally encoding as + // `text/uri-list`? + for os_str in iter { + // TODO: Is `as_encoded_bytes` correct here? + if fd + .write_all(b"\n") + .and_then(|()| fd.write_all(os_str.as_encoded_bytes())) + .is_err() + { + return; + } + } + }, + SendData::String(str) => match mime.parse_charset().unwrap_or(mime.default_charset()) { + Charset::Utf8 => { + let _ = fd.write_all(str.as_bytes()); + }, + Charset::Utf16 => { + let utf16_binary = str + .encode_utf16() + .flat_map(|uint16| uint16.to_ne_bytes()) + .collect::>(); + let _ = fd.write_all(&utf16_binary); + }, + }, + SendData::Bytes(binary) => { + let _ = fd.write_all(&binary); + }, + } + } + + // TODO: Send `DragCancel` event. + fn cancelled(&mut self, _: &Connection, _: &QueueHandle, _: &WlDataSource) { + self.dnd_state.clear_send_drag(); + } + + fn dnd_dropped( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + let _ = source; + let _ = qh; + let _ = conn; + // TODO: Send message to window. + } + + fn dnd_finished( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &wayland_client::protocol::wl_data_source::WlDataSource, + ) { + let _ = source; + let _ = qh; + let _ = conn; + // TODO: Send message to window. + self.dnd_state.clear_send_drag(); + } + + fn action( + &mut self, + conn: &Connection, + qh: &QueueHandle, + source: &WlDataSource, + action: DndAction, + ) { + let _ = action; + let _ = source; + let _ = qh; + let _ = conn; + // TODO: Send message to window + } +} + +#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)] +enum Charset { + #[default] + Utf8, + Utf16, +} + +/// 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"; + +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 = Some(()).filter(|_| downcast_failed).into_iter().flat_map(move |()| { + Self::MIME_HINT_MAP + .iter() + .filter(move |(_, haystack)| TransferType::matches(haystack, type_)) + .map(move |(mime, _)| Self { mime: mime.to_string().into(), hint: type_.hint() }) + }); + + downcast.into_iter().chain(from_hint) + } + + // TODO: We should properly parse MIME types using `mime` or a similar crate. + fn parse_charset(&self) -> Option { + let (_essence, options) = self.mime.split_once(';')?; + + let (_, charset) = options.split_once("charset=")?; + + if charset.starts_with("utf-8") { + Some(Charset::Utf8) + } else if charset.starts_with("utf-16") { + Some(Charset::Utf16) + } else { + None + } + } + + fn default_charset(&self) -> Charset { + match self.hint { + Some(TypeHint::Html) => Charset::Utf16, + _ => Charset::Utf8, + } + } + + 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)) + } + } +} + +/// In-progress typed data transfer from another application. +#[derive(Debug)] +pub struct MimeData { + mime_type: MimeType, + fd: Option, +} + +impl MimeData { + pub(crate) fn new(fd: OwnedFd, mime_type: MimeType) -> Self { + Self { mime_type, fd: Some(fd) } + } + + fn try_as_file(&mut self) -> Option { + // TODO: Is it ok that this may only work once, depending on what the fd points to? + let fd_clone = + if let Ok(cloned) = self.fd.as_ref()?.try_clone() { cloned } else { self.fd.take()? }; + Some(fd_clone.into()) + } +} + +impl TypedData for MimeData { + fn type_(&self) -> &dyn TransferType { + &self.mime_type + } + + fn try_read(&mut self) -> Option> { + Some(Box::new(BufReader::new(self.try_as_file()?))) + } + + fn try_as_uris(&mut self) -> io::Result> { + let Some(file) = self.try_as_file() else { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "This `MimeData` was already read, and the underlying file descriptor does not \ + support cloning", + )); + }; + + BufReader::new(file).lines().map(|res| res.map(OsString::from)).collect() + } + + fn try_as_string(&mut self) -> io::Result { + let Some(mut file) = self.try_as_file() else { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "This `MimeData` was already read, and the underlying file descriptor does not \ + support cloning", + )); + }; + + let charset = self.mime_type.parse_charset().unwrap_or(self.mime_type.default_charset()); + + match charset { + Charset::Utf8 => { + let mut out = String::new(); + file.read_to_string(&mut out)?; + + Ok(out) + }, + Charset::Utf16 => { + let mut bytes = Vec::::new(); + file.read_to_end(&mut bytes)?; + + // TODO: `from_utf16le` once it's stable + let utf_16 = bytes + .chunks_exact(2) + .map(|chunk| { + let arr: [u8; 2] = chunk.try_into().unwrap(); + u16::from_le_bytes(arr) + }) + .collect::>(); + + String::from_utf16(&utf_16) + .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)) + }, + } + } +} + +/// A +#[derive(Debug, Clone)] +pub struct DataOffer { + mime_types: Arc<[MimeType]>, + // TODO: Internal drag-and-drop. + data: WlDataOffer, + transfer_id: DataTransferId, + window_id: WindowId, +} + +impl DataOffer { + pub(crate) fn transfer_id(&self) -> DataTransferId { + self.transfer_id + } + + pub(crate) fn window_id(&self) -> WindowId { + self.window_id + } + + pub(crate) fn set_actions(&self, action_set: &DndActionSet) { + self.data.set_actions(action_set.dnd_actions, action_set.preferred_action()); + } + + 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 { + _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. If this is `None`, then this is + /// a purely-internal data source that will not be transferred to + /// external applications. + _data_source: Option, + /// The supplied [`DataTransferSend`]. + data: Box, + /// (Optionally) an icon for the drag-and-drop operation. + _icon: Option, +} + +impl DragSource { + pub(crate) fn new( + data_transfer_id: DataTransferId, + data_source: Option, + data: Box, + icon: Option, + ) -> Self { + Self { _data_transfer_id: data_transfer_id, _data_source: data_source, data, _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, +} + +/// The set of actions supported on a drag operation. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub struct DndActionSet { + /// The set of available actions. + pub dnd_actions: DndAction, + /// If supplied, the preferred action. If `None`, will be + /// determined from `dnd_actions`. In order of preference: + /// + /// - [`DndAction::Move`] + /// - [`DndAction::Copy`] + /// - [`DndAction::Ask`] + pub preferred_action: Option, +} + +impl From for DndActionSet { + fn from(value: DndAction) -> Self { + Self { dnd_actions: value, preferred_action: None } + } +} + +fn guess_preferred_action(action: DndAction) -> DndAction { + [DndAction::Move, DndAction::Copy, DndAction::Ask] + .into_iter() + .find(|preferred| preferred.intersects(action)) + .unwrap_or(DndAction::empty()) +} + +impl DndActionSet { + /// A new, empty `DndActionSet`. + pub fn empty() -> Self { + Self { dnd_actions: DndAction::empty(), preferred_action: None } + } + + pub(crate) fn from_dyn(mask: &dyn DndActionMask) -> Self { + mask.cast_ref::().copied().unwrap_or_else(|| mask.hint().into()) + } + + /// Get the preferred action, or guess it from `dnd_actions`. In order of preference: + /// + /// - [`DndAction::Move`] + /// - [`DndAction::Copy`] + /// - [`DndAction::Ask`] + pub fn preferred_action(&self) -> DndAction { + self.preferred_action.unwrap_or_else(|| guess_preferred_action(self.dnd_actions)) + } + + /// Get the intersection of this action set with another action set. + pub fn intersection(&self, other: &Self) -> Self { + let preferred_action = match (self.preferred_action, other.preferred_action) { + (Some(this_pref), Some(other_pref)) if this_pref.intersects(other_pref) => { + Some(this_pref.intersection(other_pref)) + }, + (Some(pref), None) | (None, Some(pref)) => Some(pref), + // If the preferences do not intersect, calculate it from the actions. + _ => None, + }; + + let dnd_actions = self.dnd_actions.intersection(other.dnd_actions); + + Self { dnd_actions, preferred_action } + } +} + +impl DndActionMask for DndActionSet { + fn hint(&self) -> DndActions { + if self.dnd_actions.is_all() { + DndActions::All + } else { + DndActions::Flags { + move_: self.dnd_actions.contains(DndAction::Move), + copy: self.dnd_actions.contains(DndAction::Copy), + link: false, + } + } + } + + fn intersection(&self, other: &dyn DndActionMask) -> Box { + Box::new(self.intersection(&Self::from_dyn(other))) + } + + fn is_empty(&self) -> bool { + self.dnd_actions.is_empty() + } + + fn intersects(&self, other: &dyn DndActionMask) -> bool { + !self.intersection(&Self::from_dyn(other)).is_empty() + } +} + +impl From for DndActionSet { + fn from(value: DndActions) -> Self { + let copy_flag = if value.copy() { DndAction::Copy } else { DndAction::empty() }; + let move_flag = if value.move_() { DndAction::Move } else { DndAction::empty() }; + + DndActionSet { dnd_actions: copy_flag | move_flag, preferred_action: None } + } +} + +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); + } + + /// 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: DndAction, + ) { + 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: DndAction, + ) { + let _ = actions; + let _ = offer; + let _ = qh; + let _ = conn; + // Not implemented, but required for `DataDeviceHandler`. + } +} + +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(), + transfer_id: make_data_transfer_id(data_device, drag.serial), + data: drag.inner().clone(), + window_id, + }); + + current_drag.set_actions(&DndActionSet::empty()); + + 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: make_data_transfer_id(data_device, drag.serial), + 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(), + ); + + self.dnd_state.receive_drag = None; + } + + 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; + }; + + 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: make_data_transfer_id(data_device, drag.serial), + position, + }, + 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 window_id = crate::make_wid(&drag.surface); + + self.events_sink.push_window_event( + WindowEvent::DragDropped { id: make_data_transfer_id(data_device, drag.serial) }, + window_id, + ); + + self.dnd_state.receive_drag = None; + } +} + +sctk::delegate_data_device!(WinitState); diff --git a/winit-wayland/src/event_loop/mod.rs b/winit-wayland/src/event_loop/mod.rs index 3a6ca6982b..ec719c7c66 100644 --- a/winit-wayland/src/event_loop/mod.rs +++ b/winit-wayland/src/event_loop/mod.rs @@ -2,34 +2,44 @@ use std::cell::{Cell, RefCell}; use std::io::Result as IOResult; -use std::mem; 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::ping::Ping; use dpi::LogicalSize; use rustix::event::{PollFd, PollFlags}; use rustix::pipe::{self, PipeFlags}; +use sctk::data_device_manager::data_offer; +use sctk::data_device_manager::data_source::DragSource as SctkDragSource; use sctk::reexports::calloop_wayland_source::WaylandSource; use sctk::reexports::client::{Connection, QueueHandle, globals}; +use sctk::shell::WaylandSurface; use tracing::warn; +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, TypedData, +}; 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, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, DragIcon, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; +use winit_core::icon::RgbaIcon; use winit_core::monitor::MonitorHandle as CoreMonitorHandle; use winit_core::window::Theme; +use crate::dnd::{DndActionSet, MimeData}; use crate::types::cursor::WaylandCustomCursor; +use crate::{DragSource, MimeType, image_to_buffer, make_data_transfer_id}; mod proxy; pub mod sink; @@ -678,8 +688,201 @@ 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, RequestError> { + let state = self.state.borrow(); + 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))?; + + current_drag + .accept(current_drag.transfer_id().into_raw() as _, Some(mime_type_str.clone())); + data_offer::receive_to_fd(current_drag, mime_type_str, writefd); + + let mime_type = mime_type.clone(); + + Ok(Box::new(MimeData::new(readfd, mime_type))) + } + + 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_actions( + &self, + id: DataTransferId, + mask: &dyn DndActionMask, + ) -> 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()); + } + + state.set_actions(&DndActionSet::from_dyn(mask)); + + Ok(()) + } + + fn start_drag( + &self, + source: WindowId, + send_data: Box, + action_mask: &dyn DndActionMask, + icon: Option, + ) -> Result { + let mut state = self.state.borrow_mut(); + let action_set = DndActionSet::from_dyn(action_mask); + + 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 windows = state.windows.borrow(); + let source_window_state = windows + .get(&source) + .ok_or(NotSupportedError::new( + "Tried to initiate drag, but source window ID was invalid", + ))? + .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( + "Tried to initiate drag, but source window does not have the pointer capability", + ))?; + let data_device = seat.data_device().ok_or(NotSupportedError::new( + "Tried to initiate drag, but source window does not have the pointer capability", + ))?; + + let serial = seat + .pointer_data() + .ok_or(NotSupportedError::new( + "Tried to initiate drag, but source window does not have the pointer capability", + ))? + // TODO: Seems like a footgun that this requires a pointer serial + .latest_button_serial(); + + let data_source = if send_data.is_internal_only() { + None + } else { + 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(()) + }); + + Some(data_device_manager.create_drag_and_drop_source( + &self.queue_handle, + mime_types, + action_set.dnd_actions, + )) + }; + + let mut pool = state.image_pool.lock().unwrap(); + let icon_surface = 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); + buffer.attach_to(&surface).ok()?; + surface.offset(icon.offset.x, icon.offset.y); + + Some(surface) + }); + + match &data_source { + Some(source) => { + source.start_drag(data_device, source_surface, icon_surface.as_ref(), serial) + }, + None => SctkDragSource::start_internal_drag( + data_device, + source_surface, + icon_surface.as_ref(), + serial, + ), + } + + let transfer_id = make_data_transfer_id(data_device.inner(), serial); + + std::mem::drop(pool); + std::mem::drop(source_window_state); + std::mem::drop(windows); + + // 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, + )); + + 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 930562b380..78d1068ee4 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::protocol::wl_surface::WlSurface; use sctk::shm::slot::{Buffer, CreateBufferError, SlotPool}; +use wayland_client::protocol::wl_data_device::WlDataDevice; 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, DndActionSet, 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: &WlDataDevice, 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 ddf0e061df..aa7c6f33b7 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; @@ -60,6 +61,9 @@ pub struct WinitSeatState { /// The pinch pointer gesture bound on the seat. pointer_gesture_pinch: Option, + /// The drag-and-drop state + data_device: Option, + /// The keyboard bound on the seat. keyboard_state: Option, @@ -74,6 +78,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 { @@ -125,6 +137,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(), @@ -209,6 +226,8 @@ impl SeatHandler for WinitState { pointer_gesture_pinch.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 11970d7069..6e9aa20092 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 8465b720d6..c130d57398 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 577c8c42ec..8c06edc2f8 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 dcac4b69d1..fde54f27b5 100644 --- a/winit-wayland/src/window/state.rs +++ b/winit-wayland/src/window/state.rs @@ -80,6 +80,10 @@ pub struct WindowState { /// Queue handle. pub queue_handle: QueueHandle, + pub last_seat: Option, + + pub last_event_serial: Option, + /// Theme variant. theme: Option, @@ -225,6 +229,8 @@ impl WindowState { min_surface_size: MIN_WINDOW_SIZE, resize_increments: None, pointer_constraints, + last_seat: None, + last_event_serial: None, pointers: Default::default(), queue_handle: queue_handle.clone(), resizable: true, @@ -243,6 +249,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 5a5df3ff9a..3564a0d9c1 100644 --- a/winit-win32/Cargo.toml +++ b/winit-win32/Cargo.toml @@ -32,7 +32,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 c5225cfee9..9bf87f4b8f 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 0000000000..005377fb9b --- /dev/null +++ b/winit-win32/src/dnd.rs @@ -0,0 +1,1557 @@ +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::ffi::{OsString, c_void}; +use std::io; +use std::ops::ControlFlow; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; +use std::ptr::NonNull; +use std::rc::Rc; +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::DndActions; + +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; + +#[derive(Debug)] +enum DataKind { + Uris(Vec), + String(String), + Bytes(Vec), +} + +// 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, DataKind::String(text)); + } + + if let Some(uris) = unsafe { read_uri_list(data_obj) } { + if !uris.is_empty() { + data.insert(TypeHint::UriList, DataKind::Uris(uris)); + } + } + + if let Some(png) = unsafe { read_png(data_obj) } { + if !png.is_empty() { + data.insert(TypeHint::Image { extension_hint: Some("png") }, DataKind::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) }; + + paths.push(OsString::from_wide(&path_buf)); + } + + 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: Rc, +} + +impl WinDataTransfer { + pub(crate) fn new(data: Rc) -> 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: Rc, +} + +impl WinTypedData { + pub(crate) fn new(data: Rc, 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(&mut self) -> Option> { + match self.data.data.get(&self.type_)? { + DataKind::Bytes(bytes) => Some(Box::new(io::Cursor::new(bytes.clone()))), + DataKind::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. + DataKind::Uris(_) => None, + } + } + + fn try_as_uris(&mut self) -> io::Result> { + match self.data.data.get(&self.type_) { + Some(DataKind::Uris(uris)) => Ok(uris.clone()), + _ => Err(io::ErrorKind::InvalidData.into()), + } + } + + fn try_as_string(&mut self) -> io::Result { + match self.data.data.get(&self.type_) { + Some(DataKind::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, initial_actions) = match drop_handler.runner.source_drag.get() { + Some(info) => (info.id, info.allowed_actions), + None => (next_data_transfer_id(), DndActions::none()), + }; + drop_handler.active_data_transfer_id = Some(data_transfer_id); + + let data = Rc::new(unsafe { DataObject::from_idataobject(pDataObj) }); + drop_handler.runner.register_data_transfer(data_transfer_id, data, initial_actions); + + 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), + }); + unsafe { + *pdwEffect = DROPEFFECT_NONE; + } + + // 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 = DROPEFFECT_NONE; + } + + return E_ABORT; + }; + + let actions = drop_handler.runner.current_drag_actions(data_transfer_id); + let source_allowed = unsafe { *pdwEffect }; + + 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::DragPosition { id: data_transfer_id, position }); + let new_effect = pick_effect(actions, grfKeyState, source_allowed); + unsafe { + *pdwEffect = 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 actions = drop_handler.runner.current_drag_actions(data_transfer_id); + let source_allowed = unsafe { *pdwEffect }; + + 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; + + // 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 position = PhysicalPosition::new(pt.x as f64, pt.y as f64); + (drop_handler.send_event)(WindowEvent::DragPosition { id: data_transfer_id, position }); + let event = if effect == DROPEFFECT_NONE { + WindowEvent::DragLeft { id: data_transfer_id } + } else { + WindowEvent::DragDropped { id: data_transfer_id } + }; + (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, +}; + +/// Map the app's [`DndActions`] to the win32 `DROPEFFECT_*` bitmask. +pub(crate) fn actions_to_dropeffect_mask(actions: DndActions) -> u32 { + let mut mask = 0u32; + if actions.copy() { + mask |= DROPEFFECT_COPY; + } + if actions.move_() { + mask |= DROPEFFECT_MOVE; + } + if actions.link() { + mask |= DROPEFFECT_LINK; + } + mask +} + +// Intersect the app's valid actions with the source's allowed effects, honoring Ctrl/Shift. +fn pick_effect(actions: DndActions, key_state: u32, source_allowed: u32) -> u32 { + const MK_SHIFT: u32 = 0x0004; + const MK_CONTROL: u32 = 0x0008; + + let allowed = actions_to_dropeffect_mask(actions) & source_allowed; + if allowed == 0 { + return DROPEFFECT_NONE; + } + + let ctrl = key_state & MK_CONTROL != 0; + let shift = key_state & MK_SHIFT != 0; + if ctrl && shift && (allowed & DROPEFFECT_LINK) != 0 { + return DROPEFFECT_LINK; + } + if ctrl && !shift && (allowed & DROPEFFECT_COPY) != 0 { + return DROPEFFECT_COPY; + } + if !ctrl && shift && (allowed & DROPEFFECT_MOVE) != 0 { + return DROPEFFECT_MOVE; + } + + if (allowed & DROPEFFECT_COPY) != 0 { + DROPEFFECT_COPY + } else if (allowed & DROPEFFECT_MOVE) != 0 { + DROPEFFECT_MOVE + } else { + DROPEFFECT_LINK + } +} + +// ============================================================================ +// 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 = 'uri_to_path: { + if let Some(path_str) = path.to_str() { + // There's no `strip_prefix` etc on `OsStr` so we need to go via `str` + // Windows is the only platform that sends raw file paths instead of URIs + let Some(path_str) = path_str.strip_prefix("file://") else { + break 'uri_to_path path; + }; + + // Even though "/" is theoretically a valid path separator on Windows, it + // doesn't seem to work for drag-and-drop specifically. + OsString::from(path_str.replace("/", "\\")) + } else { + path + } + }; + + wide.extend(path.encode_wide()); + 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: dpi::PhysicalPosition, +) -> 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 19a23c9520..0000000000 --- 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 8f1008f98d..76adf8fbf1 100644 --- a/winit-win32/src/event_loop.rs +++ b/winit-win32/src/event_loop.rs @@ -13,13 +13,16 @@ use std::{fmt, mem, panic, ptr}; use dpi::{PhysicalPosition, PhysicalSize}; use windows_sys::Win32::Foundation::{ - FALSE, GetLastError, HANDLE, HWND, LPARAM, LRESULT, POINT, RECT, WAIT_FAILED, WPARAM, + DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, FALSE, GetLastError, HANDLE, HWND, LPARAM, LRESULT, POINT, + RECT, WAIT_FAILED, WPARAM, }; use windows_sys::Win32::Graphics::Gdi::{ GetMonitorInfoW, MONITOR_DEFAULTTONULL, MONITORINFO, MonitorFromRect, MonitorFromWindow, RDW_INTERNALPAINT, RedrawWindow, SC_SCREENSAVE, ScreenToClient, ValidateRect, }; -use windows_sys::Win32::System::Ole::RevokeDragDrop; +use windows_sys::Win32::System::Ole::{ + DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, DoDragDrop, RevokeDragDrop, +}; use windows_sys::Win32::System::Threading::{ CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, CreateWaitableTimerExW, GetCurrentThreadId, INFINITE, SetWaitableTimer, TIMER_ALL_ACCESS, @@ -63,6 +66,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,7 +76,7 @@ use winit_core::event::{ }; use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::{ - ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, DragIcon, EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; @@ -82,8 +88,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::SourceDrag; use crate::icon::WinCursor; use crate::ime::ImeContext; use crate::keyboard::KeyEventBuilder; @@ -478,6 +485,124 @@ 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, RequestError> { + let Some(data) = self.0.data_transfer(id) else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + let hint = type_.hint().ok_or(RequestError::Ignored)?; + + WinTypedData::new(data, hint).map(|value| Box::new(value) as _).ok_or(RequestError::Ignored) + } + + fn data_transfer(&self, id: DataTransferId) -> Result, RequestError> { + let Some(data) = self.0.data_transfer(id) else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + + Ok(Box::new(WinDataTransfer::new(data))) + } + + fn set_valid_actions( + &self, + id: DataTransferId, + actions: &dyn DndActionMask, + ) -> 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.hint(); + Ok(()) + } + + fn start_drag( + &self, + _source: WindowId, + send_data: Box, + action_mask: &dyn DndActionMask, + icon: Option, + ) -> Result { + let allowed_actions = action_mask.hint(); + let allowed_effects = crate::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, + ) + }; + 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"); + } + } + + // 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.0.source_drag.set(Some(SourceDrag { id, allowed_actions })); + let _guard = ClearOnDrop(&self.0.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, + ) + }; + + // 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. + if hr == DRAGDROP_S_DROP || hr == DRAGDROP_S_CANCEL { + tracing::trace!( + "DoDragDrop completed: hr=0x{hr:08x} effect_out={effect_out} \ + (COPY={DROPEFFECT_COPY}, MOVE={DROPEFFECT_MOVE}, LINK={DROPEFFECT_LINK})", + ); + Ok(id) + } else { + Err(os_error!(std::io::Error::other(format!("DoDragDrop failed: 0x{hr:08x}"))).into()) + } + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { @@ -487,6 +612,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 +1315,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(); diff --git a/winit-win32/src/event_loop/runner.rs b/winit-win32/src/event_loop/runner.rs index 65e6082761..51bcb973c4 100644 --- a/winit-win32/src/event_loop/runner.rs +++ b/winit-win32/src/event_loop/runner.rs @@ -9,16 +9,35 @@ use std::{fmt, mem, panic}; use dpi::PhysicalSize; use windows_sys::Win32::Foundation::HWND; 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, DndActions}; use winit_core::window::WindowId; use super::{ActiveEventLoop, ControlFlow, EventLoopThreadExecutor}; +use crate::dnd::DataObject; 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). +pub(super) struct DragState { + pub(super) id: DataTransferId, + data: Rc, + pub(super) actions: DndActions, +} + +/// 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) allowed_actions: DndActions, +} + pub(crate) struct EventLoopRunner { pub(super) thread_id: u32, @@ -37,6 +56,21 @@ 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>, + + /// 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 +121,46 @@ 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_source_drag_cleanup: Cell::new(None), } } + 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, + data: Rc, + actions: DndActions, + ) { + *self.drag_state.borrow_mut() = Some(DragState { id, data, actions }); + } + + 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(crate) fn data_transfer(&self, id: DataTransferId) -> Option> { + let state = self.drag_state.borrow(); + state.as_ref().filter(|s| s.id == id).map(|s| s.data.clone()) + } + + pub(crate) fn current_drag_actions(&self, id: DataTransferId) -> DndActions { + self.drag_state + .borrow() + .as_ref() + .filter(|s| s.id == id) + .map(|s| s.actions) + .unwrap_or(DndActions::none()) + } + /// Associate the application's event handler with the runner. /// /// # Safety @@ -138,12 +209,18 @@ impl EventLoopRunner { last_events_cleared: _, event_handler, event_buffer: _, + drag_state, + source_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.borrow_mut() = None; + source_drag.set(None); + pending_source_drag_cleanup.set(None); } } @@ -285,6 +362,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 569d1f32c1..4693875b43 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 dba95e9bd8..34e6f83a3e 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 ed06d76042..eefb654b03 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/Cargo.toml b/winit-x11/Cargo.toml index 519904a268..9f8fa096fd 100644 --- a/winit-x11/Cargo.toml +++ b/winit-x11/Cargo.toml @@ -15,6 +15,7 @@ serde = ["dep:serde", "bitflags/serde", "smol_str/serde", "dpi/serde"] bitflags.workspace = true cursor-icon.workspace = true dpi.workspace = true +rustversion.workspace = true rwh_06.workspace = true serde = { workspace = true, optional = true } smol_str.workspace = true diff --git a/winit-x11/src/atoms.rs b/winit-x11/src/atoms.rs index e5ac78f4dd..24d9091766 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,7 +34,10 @@ macro_rules! atom_manager { atom_manager! { // General Use Atoms CARD32, + STRING, UTF8_STRING, + TARGETS, + SAVE_TARGETS, WM_CHANGE_STATE, WM_CLIENT_MACHINE, WM_DELETE_WINDOW, @@ -91,7 +94,35 @@ atom_manager! { 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 3c85e4fe2e..a2eb9c2da7 100644 --- a/winit-x11/src/dnd.rs +++ b/winit-x11/src/dnd.rs @@ -1,11 +1,15 @@ +use std::cell::Cell; +use std::ffi::OsString; use std::io; +use std::marker::PhantomData; use std::os::raw::*; -use std::path::{Path, PathBuf}; use std::str::Utf8Error; -use std::sync::Arc; +use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::{Arc, OnceLock, RwLock}; +use std::thread::ThreadId; -use dpi::PhysicalPosition; use percent_encoding::percent_decode; +use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData}; use x11rb::protocol::xproto::{self, ConnectionExt}; use crate::atoms::AtomName::None as DndNone; @@ -21,98 +25,483 @@ 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) + } +} + +// When `thread_id_value` is stabilized, this can become `AtomicU64`. +#[derive(Default, Debug)] +pub struct DeadlockSentinel(Arc>>); + +/// Read-side of `DeadlockSentinel` (to prevent accidentally guarding in a re-entrant way). +#[derive(Debug, Clone)] +pub struct DeadlockSentinelReader(Arc>>); + +impl DeadlockSentinelReader { + fn get(&self) -> Option { + *self.0.read().unwrap() + } +} + +#[must_use] +#[derive(Debug)] +pub struct DeadlockSentinelGuard(Arc>>); + +impl Drop for DeadlockSentinelGuard { + fn drop(&mut self) { + *self.0.write().unwrap() = None; + } +} + +impl DeadlockSentinel { + pub fn guard(&self) -> DeadlockSentinelGuard { + let mut writer = self.0.write().unwrap(); + assert!(writer.is_none(), "Internal error: re-entrant `DeadlockSentinelGuard`"); + *writer = Some(std::thread::current().id()); + DeadlockSentinelGuard(self.0.clone()) + } + + pub fn reader(&self) -> DeadlockSentinelReader { + DeadlockSentinelReader(self.0.clone()) + } +} + +#[derive(Debug, Default)] +struct SharedDataInnerState { + data: OnceLock, io::ErrorKind>>, +} + +impl SharedDataInnerState { + fn has_data(&self) -> bool { + self.data.get().is_some() + } + + fn try_data(&self) -> io::Result<&[u8]> { + self.data + .get() + .map(|data| data.as_ref().map(|data| &**data).map_err(|err| io::Error::from(*err))) + .ok_or_else(|| io::Error::from(io::ErrorKind::WouldBlock))? + } +} + +#[derive(Clone, Debug)] +pub(crate) struct SharedDataReader { + reader: Arc, + deadlock_sentinel: DeadlockSentinelReader, +} + +impl SharedDataReader { + fn try_data(&self) -> io::Result<&[u8]> { + self.wait_for_data()?; + self.reader.try_data() + } + + #[rustversion::since(1.86)] + fn wait_internal(&self) -> io::Result<()> { + let _ = self.reader.data.wait(); + + Ok(()) + } + + #[rustversion::before(1.86)] + fn wait_internal(&self) -> io::Result<()> { + if self.reader.has_data() { Ok(()) } else { Err(io::ErrorKind::WouldBlock.into()) } + } + + fn wait_for_data(&self) -> io::Result<()> { + if !self.reader.has_data() + && self.deadlock_sentinel.get() == Some(std::thread::current().id()) + { + return Err(io::ErrorKind::Deadlock.into()); + } + + self.wait_internal() + } +} + +type NonSyncMarker = PhantomData>; + +#[derive(Debug, Default)] +pub(crate) struct SharedDataWriter { + writer: Arc, + _non_sync: NonSyncMarker, +} + +impl SharedDataWriter { + fn reader(&self, deadlock_sentinel: DeadlockSentinelReader) -> SharedDataReader { + SharedDataReader { reader: self.writer.clone(), deadlock_sentinel } + } + + pub(crate) fn write(&self, value: Box<[c_uchar]>) -> Result<(), Box<[c_uchar]>> { + // We know that we just passed `Ok`, so we can unwrap here. + self.writer.data.set(Ok(value)).map_err(|result| result.unwrap()) + } +} + +impl Drop for SharedDataWriter { + fn drop(&mut self) { + // Prevent `SelectionReader::wait_for_data` from deadlocking. + let _ = self.writer.data.set(Err(io::ErrorKind::BrokenPipe)); + } +} + +#[derive(Clone, Debug)] +pub struct SelectionReader { + type_: SelectionType, + data: SharedDataReader, + pos: u64, +} + +impl io::Read for SelectionReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.with_cursor(|cursor| cursor.read(buf)) + } + + fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { + self.with_cursor(|cursor| cursor.read_to_end(buf)) + } + + fn read_to_string(&mut self, buf: &mut String) -> io::Result { + self.with_cursor(|cursor| cursor.read_to_string(buf)) + } + + fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> { + self.with_cursor(|cursor| cursor.read_exact(buf)) + } +} + +impl io::BufRead for SelectionReader { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + // `io::Cursor::split` takes `&self` instead of `self`, so we need to reimplement it here. + let data = self.data.try_data()?; + + Ok(&data[self.pos.min(data.len() as u64) as usize..]) + } + + fn consume(&mut self, amount: usize) { + // `io::Cursor::consume` doesn't require a buffer, so we skip the `try_data` check implied + // by `with_cursor`. + self.pos += amount as u64; + } + + fn read_line(&mut self, buf: &mut String) -> io::Result { + self.with_cursor(|cursor| cursor.read_line(buf)) + } +} + +impl SelectionReader { + pub(crate) fn new(type_: SelectionType, data: SharedDataReader) -> Self { + Self { type_, data, pos: 0 } + } + + // Instead of reimplementing `io::Cursor`, we have a maximally-conservative + // implementation that just synchronizes state in order to prevent the chance + // of misimplementation. + fn with_cursor(&mut self, func: F) -> io::Result + where + F: FnOnce(&mut io::Cursor<&[u8]>) -> io::Result, + { + let data = self.data.try_data()?; + + let mut cursor = io::Cursor::new(data); + cursor.set_position(self.pos); + let result = func(&mut cursor)?; + let new_pos = cursor.position(); + self.pos = new_pos; + + Ok(result) + } +} + +impl TypedData for SelectionReader { + fn try_read(&mut self) -> Option> { + Some(Box::new(self.clone())) + } + + fn type_(&self) -> &dyn TransferType { + &self.type_ + } + + fn try_as_string(&mut 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) => { + let data = self.data.try_data()?; + + // TODO: Is it correct to default to UTF-8? + let charset = self.type_.charset.unwrap_or(Charset::Utf8); + + match charset { + Charset::Utf16 => decode_utf16_bytes(data), + Charset::Utf8 => std::str::from_utf8(data) + .map(|str| str.to_owned()) + .map_err(invalid_data) + .or_else(|_| decode_utf16_bytes(data)), + } + }, + Some(TypeHint::UriList) => { + let data = self.data.try_data()?; + + percent_decode(data).decode_utf8().map(Into::into).map_err(invalid_data) + }, + _ => Err(io::ErrorKind::InvalidData.into()), + } + } + + fn try_as_uris(&mut 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(Into::into) + .collect()) + } +} + +#[derive(Debug)] +pub(crate) struct SelectionFetchState { + type_: SelectionType, + // Populated by SelectionNotify event handler + value: SharedDataWriter, +} + +impl SelectionFetchState { + pub(crate) fn new(type_: SelectionType) -> Self { + Self { type_, value: Default::default() } + } + + pub(crate) fn type_(&self) -> &SelectionType { + &self.type_ + } + + pub(crate) fn as_reader(&self, sentinel: DeadlockSentinelReader) -> SelectionReader { + SelectionReader::new(self.type_().clone(), self.value.reader(sentinel)) + } +} + +#[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 last_fetched_selection: Option, + /// 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(), + last_fetched_selection: Default::default(), + 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, + pub deadlock_sentinel: DeadlockSentinel, + // If `None`, no drag operation is in progress. + pub state: Option, +} + +#[derive(Debug)] +pub struct Selection { + types: Arc<[SelectionType]>, +} + +impl Selection { + pub(crate) fn new(types: Arc<[SelectionType]>) -> Selection { + Selection { types } + } +} +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] +enum Charset { + Utf8, + Utf16, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct SelectionType { + hint: Option, + atom: xproto::Atom, + charset: Option, +} + +impl SelectionType { + pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self { + let atom_to_hint = [ + // Files + (atoms[TextUriList], TypeHint::UriList, None), + (atoms[TARGETS], TypeHint::UriList, None), + (atoms[SAVE_TARGETS], TypeHint::UriList, None), + // Plaintext + (atoms[STRING], TypeHint::Plaintext, None), + (atoms[UTF8_STRING], TypeHint::Plaintext, None), + (atoms[TextPlain], TypeHint::Plaintext, Some(Charset::Utf16)), + (atoms[TextPlainCharsetUtf8], TypeHint::Plaintext, Some(Charset::Utf8)), + // HTML + (atoms[TextHtml], TypeHint::Html, Some(Charset::Utf16)), + (atoms[TextHtmlCharsetUtf8], TypeHint::Html, Some(Charset::Utf8)), + // RTF + (atoms[ApplicationRtf], TypeHint::Rtf, None), + // Audio + (atoms[AudioAac], TypeHint::Audio { extension_hint: Some("aac") }, None), + (atoms[AudioAiff], TypeHint::Audio { extension_hint: Some("aif") }, None), + (atoms[AudioFlac], TypeHint::Audio { extension_hint: Some("flac") }, None), + (atoms[AudioVndWav], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioVndWave], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioWav], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioWave], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioXWav], TypeHint::Audio { extension_hint: Some("wav") }, None), + (atoms[AudioOgg], TypeHint::Audio { extension_hint: Some("ogg") }, None), + (atoms[AudioMpeg], TypeHint::Audio { extension_hint: Some("mp3") }, None), + // Image + (atoms[ImageBmp], TypeHint::Image { extension_hint: Some("bmp") }, None), + (atoms[ImageGif], TypeHint::Image { extension_hint: Some("gif") }, None), + (atoms[ImageJpeg], TypeHint::Image { extension_hint: Some("jpg") }, None), + (atoms[ImagePjpeg], TypeHint::Image { extension_hint: Some("jpg") }, None), + (atoms[ImagePng], TypeHint::Image { extension_hint: Some("png") }, None), + (atoms[ImageRaw], TypeHint::Image { extension_hint: Some("raw") }, None), + (atoms[ImageSvg], TypeHint::Image { extension_hint: Some("svg") }, None), + (atoms[ImageTiff], TypeHint::Image { extension_hint: Some("tiff") }, None), + (atoms[ImageWebp], TypeHint::Image { extension_hint: Some("webp") }, None), + (atoms[ImageXIcon], TypeHint::Image { extension_hint: Some("ico") }, None), + ]; + let hint_and_charset = atom_to_hint.iter().find_map(|(haystack, hint, charset)| { + (*haystack == atom).then_some((Some(*hint), *charset)) + }); + + let (hint, charset) = hint_and_charset.unwrap_or((None, None)); + + Self { hint, charset, 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, deadlock_sentinel: DeadlockSentinel) -> Self { + Dnd { xconn, state: None, deadlock_sentinel } + } + + 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 reset(&mut self) { - self.version = None; - self.type_list = None; - self.source_window = None; - self.result = None; - self.dragging = false; + self.state = None; } - pub unsafe fn send_status( - &self, - this_window: xproto::Window, + 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[XdndActionPrivate]) } else { (0, atoms[DndNone]) }; self.xconn .send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [ this_window, @@ -138,54 +527,57 @@ impl Dnd { ) } - pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) { + pub unsafe 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, - window: xproto::Window, - ) -> Result, util::GetPropertyError> { + this_window: xproto::Window, + target_window: xproto::Window, + status: DndState, + ) -> Result<(), X11Error> { let atoms = self.xconn.atoms(); - self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList]) - } - - 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) - } + let (accepted, action) = match status { + 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(()) + } + pub unsafe fn read_data(&self, window: xproto::Window) -> Result<(), util::GetPropertyError> { + let state = self.state.as_ref().ok_or(util::GetPropertyError::Unknown)?; + + // Never fetched + let last_fetch = + state.last_fetched_selection.as_ref().ok_or(util::GetPropertyError::Unknown)?; + + let atoms = self.xconn.atoms(); + let type_ = last_fetch.type_.atom(); + let bytes = self.xconn.get_property(window, atoms[XdndSelection], type_)?; + + last_fetch.value.write(bytes.into()).map_err(|_| util::GetPropertyError::Unknown)?; + + Ok(()) } } diff --git a/winit-x11/src/event_loop.rs b/winit-x11/src/event_loop.rs index b89a06c1e1..2b15a556e9 100644 --- a/winit-x11/src/event_loop.rs +++ b/winit-x11/src/event_loop.rs @@ -19,11 +19,12 @@ 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, TypedData}; +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, + ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents, DndActionMask, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider, OwnedDisplayHandle as CoreOwnedDisplayHandle, }; @@ -37,13 +38,13 @@ use x11rb::x11_utils::X11Error as LogicalError; use x11rb::xcb_ffi::ReplyOrIdError; use crate::atoms::*; -use crate::dnd::Dnd; +use crate::dnd::{DeadlockSentinelGuard, Dnd, SelectionFetchState}; 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 +169,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 +228,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), Default::default()).into(); let (ime_sender, ime_receiver) = mpsc::channel(); let (ime_event_sender, ime_event_receiver) = mpsc::channel(); @@ -342,6 +343,7 @@ impl EventLoop { let window_target = ActiveEventLoop { ime, + dnd, root, control_flow: Cell::new(ControlFlow::default()), exit: Cell::new(None), @@ -368,7 +370,6 @@ impl EventLoop { let event_processor = EventProcessor { target: window_target, - dnd, devices: Default::default(), randr_event_offset, ime_receiver, @@ -558,6 +559,8 @@ impl EventLoop { } fn single_iteration(&mut self, app: &mut A, cause: StartCause) { + let _guard = self.event_processor.target.selection_deadlock_guard(); + app.new_events(&self.event_processor.target, cause); // NB: For consistency all platforms must call `can_create_surfaces` even though X11 @@ -621,9 +624,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); } } @@ -663,6 +665,10 @@ impl ActiveEventLoop { &self.xconn } + pub(crate) fn selection_deadlock_guard(&self) -> DeadlockSentinelGuard { + self.dnd.borrow().deadlock_sentinel.guard() + } + /// Update the device event based on window focus. pub fn update_listen_device_events(&self, focus: bool) { let device_events = self.device_events.get() == DeviceEvents::Always @@ -759,6 +765,93 @@ 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.as_ref().is_none_or(|state| state.transfer_id != id) { + return Err(RequestError::Ignored); + } + + let Some(state) = dnd.state.as_ref() else { + return Err(RequestError::Ignored); + }; + + Ok(Box::new(Selection::new(state.types.clone()))) + } + + fn fetch_data_transfer( + &self, + id: DataTransferId, + type_: &dyn TransferType, + ) -> Result, RequestError> { + let mut dnd = self.dnd.borrow_mut(); + + if dnd.state.as_ref().is_none_or(|state| state.transfer_id != id) { + return Err(RequestError::NotSupported(NotSupportedError::new( + "Unknown data transfer", + ))); + } + + 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 deadlock_sentinel = dnd.deadlock_sentinel.reader(); + + let mut new_state = dnd + .state + .as_mut() + .ok_or(RequestError::Ignored)? + .last_fetched_selection + .take() + .filter(|state| state.type_() == &type_); + + let Some(target_window) = dnd.state.as_ref().map(|s| s.target_window) else { + return Err(RequestError::Ignored); + }; + + let reader = new_state + .get_or_insert_with(|| { + // This results in the `SelectionNotify` event + unsafe { + // TODO: Handle this better + dnd.convert_selection(target_window, self.xconn.timestamp(), type_.atom()); + } + + SelectionFetchState::new(type_) + }) + .as_reader(deadlock_sentinel); + + let Some(state) = dnd.state.as_mut() else { + return Err(RequestError::Ignored); + }; + + state.last_fetched_selection = new_state; + + Ok(Box::new(reader)) + } + + fn set_valid_actions( + &self, + id: DataTransferId, + actions: &dyn DndActionMask, + ) -> Result<(), RequestError> { + let mut dnd = self.dnd.borrow_mut(); + + let Some(state) = &mut dnd.state else { + return Err(os_error!(UnknownDataTransfer(id)).into()); + }; + + if state.transfer_id != id { + return Err(os_error!(UnknownDataTransfer(id)).into()); + } + + state.accepted = actions.hint().any(); + + Ok(()) + } } impl rwh_06::HasDisplayHandle for ActiveEventLoop { @@ -767,6 +860,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 72c1c295f7..37cd51f669 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::{ @@ -31,7 +33,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 +51,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 +295,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 +308,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 +428,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,122 +489,102 @@ 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); - - // This results in the `SelectionNotify` event below - unsafe { - self.dnd.convert_selection(window, 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), + }); - 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 { + return; + }; + let Some(source_window) = dnd.state.as_ref().map(|s| s.source_window) else { + warn!("Received `XdndDrop` without `XdndEnter`"); + return; + }; + + (source_window, state.transfer_id) }; + app.window_event(&self.target, window_id, WindowEvent::DragDropped { id: transfer_id }); + + let mut dnd = self.target.dnd.borrow_mut(); + unsafe { - self.dnd - .send_finished(window, source_window, state) + dnd.send_finished(window, source_window) .expect("Failed to send `XdndFinished` message."); } - self.dnd.reset(); + 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) { + // TODO: Should we have an explicit notification for when the selection is ready? + fn selection_notify(&mut self, xev: &XSelectionEvent, _: &mut dyn ApplicationHandler) { let atoms = self.target.xconn.atoms(); let window = xev.requestor as xproto::Window; - let window_id = mkwid(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); - - 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 } - }; - - app.window_event(&self.target, window_id, event); - } - - self.dnd.result = Some(parse_result); - } + let _ = unsafe { self.target.dnd.borrow().read_data(window) }; } fn configure_notify(&self, xev: &XConfigureEvent, app: &mut dyn ApplicationHandler) { @@ -1757,7 +1762,7 @@ impl EventProcessor { .find(|prev_monitor| prev_monitor.name == new_monitor.name) .map(|prev_monitor| prev_monitor.scale_factor); if Some(new_monitor.scale_factor) != maybe_prev_scale_factor { - for window in self.target.windows.borrow().iter().filter_map(|(_, w)| w.upgrade()) { + for window in self.target.windows.borrow().values().filter_map(|w| w.upgrade()) { window.refresh_dpi_for_monitor( &new_monitor, maybe_prev_scale_factor, diff --git a/winit-x11/src/lib.rs b/winit-x11/src/lib.rs index 41bb1382e6..79c9d05e8a 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 7329e7132f..0f89a0d9db 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 0277732110..4c1a0f3562 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/examples/application.rs b/winit/examples/application.rs index 4098ebff68..3d4e865ebf 100644 --- a/winit/examples/application.rs +++ b/winit/examples/application.rs @@ -546,7 +546,7 @@ impl ApplicationHandler for Application { | WindowEvent::KeyboardInput { .. } | WindowEvent::PointerEntered { .. } | WindowEvent::DragEntered { .. } - | WindowEvent::DragMoved { .. } + | WindowEvent::DragPosition { .. } | WindowEvent::DragDropped { .. } | WindowEvent::Destroyed | WindowEvent::Ime(_) diff --git a/winit/examples/child_window.rs b/winit/examples/child_window.rs index 4077bca36b..d1427c2c62 100644 --- a/winit/examples/child_window.rs +++ b/winit/examples/child_window.rs @@ -56,7 +56,7 @@ fn main() -> Result<(), impl std::error::Error> { self.windows.clear(); event_loop.exit(); }, - WindowEvent::PointerEntered { device_id: _, .. } => { + WindowEvent::PointerEntered { .. } => { // On x11, log when the cursor entered in a window even if the child window // is created by some key inputs. // the child windows are always placed at (0, 0) with size (200, 200) in the diff --git a/winit/examples/dnd.rs b/winit/examples/dnd.rs index c8d0332656..7d7f7c2847 100644 --- a/winit/examples/dnd.rs +++ b/winit/examples/dnd.rs @@ -1,9 +1,16 @@ use std::error::Error; +use std::ffi::OsString; +use std::path::PathBuf; +use std::sync::Arc; -use tracing::info; +use dpi::PhysicalPosition; +use image::RgbImage; +use tracing::{error, info, warn}; use winit::application::ApplicationHandler; -use winit::event::WindowEvent; -use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::data_transfer::{DataTransferId, DataTransferSendBuilder, TypeHint, TypedData}; +use winit::event::{MouseButton, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, DndActions, DragIcon, EventLoop}; +use winit::icon::{Icon, RgbaIcon}; use winit::window::{Window, WindowAttributes, WindowId}; #[path = "util/fill.rs"] @@ -24,34 +31,218 @@ fn main() -> Result<(), Box> { #[derive(Debug)] struct Application { window: Option>, + last_dnd_fetch: Option>, + last_drag_start: Option, + drag_icon: (Icon, PhysicalPosition), + drag_image_data: Arc, } +const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png"); + impl Application { fn new() -> Self { - Self { window: None } + let drag_icon = load_icon(DRAG_IMAGE); + let drag_image_data = Arc::new(image::load_from_memory(DRAG_IMAGE).unwrap().into_rgb8()); + Self { + window: None, + last_dnd_fetch: None, + last_drag_start: None, + drag_icon, + drag_image_data, + } } } +fn load_icon(bytes: &[u8]) -> (Icon, PhysicalPosition) { + 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(), + PhysicalPosition { x: -(icon_width as i32) / 2, y: -(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!"); self.window = Some(event_loop.create_window(window_attributes).unwrap()); } 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, state, .. } => { + let Some(button) = button.mouse_button() else { + return; + }; + + if button == MouseButton::Left && state.is_pressed() { + let (icon, offset) = 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"); + let icon_file = icon_file.display(); + Some(vec![OsString::from(format!("file://{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 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(), + &DndActions::All, + Some(DragIcon { icon, offset }), + ); + + self.last_drag_start = result.ok(); + } + }, + WindowEvent::DragLeft { .. } => { + info!("{event:?}"); + self.last_dnd_fetch = None; + }, + WindowEvent::DragPosition { .. } => { info!("{event:?}"); }, + WindowEvent::DragDropped { .. } => { + info!("{event:?}"); + + if let Some(data) = &mut self.last_dnd_fetch { + match data.type_().hint() { + Some(TypeHint::Plaintext | TypeHint::Html) => { + let text = data.try_as_string().unwrap(); + info!("{text:?}"); + }, + Some(TypeHint::UriList) => { + let uris = data.try_as_uris().unwrap(); + info!("{uris:#?}"); + }, + Some(TypeHint::Image { extension_hint: ext }) => { + let mut bytes = Vec::new(); + 'read_image: { + if data.try_read().unwrap().read_to_end(&mut bytes).is_err() { + warn!("Could not read!"); + break 'read_image; + } + + 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!"); + }, + } + } + + self.last_dnd_fetch = None; + }, + 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_actions(id, &DndActions::none()).unwrap(); + return; + }; + + event_loop.set_valid_actions(id, &DndActions::all()).unwrap(); + + self.last_dnd_fetch = event_loop.fetch_data_transfer(id, &type_).ok(); + + match self.last_dnd_fetch.as_mut().unwrap().try_as_string() { + Err(e) if e.kind() == std::io::ErrorKind::Deadlock => { + warn!( + "Immediately waiting for a fetched data transfer may deadlock on some \ + platforms!" + ); + }, + _ => {}, + } + }, WindowEvent::RedrawRequested => { let window = self.window.as_ref().unwrap(); window.pre_present_notify(); diff --git a/winit/src/lib.rs b/winit/src/lib.rs index beb5c809ec..bab3fd9900 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;