New drag and drop API (#4571)

This commit implements a new API for drag and drop, with a `DataTransfer` type which abstracts over the various clipboard/drag and drop APIs across different platforms. I built this on top of #2429 although admittedly I ended up removing pretty much all of their work while I was reworking the design.

This is being built in order to help support [drag-and-drop work](https://github.com/slint-ui/slint/issues/1967) in Slint's winit backend. As part of that work, I did extensive research on how drag-and-drop and clipboard APIs are implemented across different platforms, and wrote a (still WIP) research document that can be found [here](https://gist.github.com/eira-fransham/06750cf8d25ade08d362a0ca8dfafe06).

The new API is inspired by the browser's [`DataTransfer`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer) API.
This commit is contained in:
Eira Fransham
2026-07-16 14:48:12 +02:00
committed by GitHub
parent 066c091b56
commit 156433eb91
44 changed files with 5905 additions and 594 deletions

View File

@@ -36,6 +36,7 @@ rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"] }
serde = { version = "1", features = ["serde_derive"] }
smol_str = "0.3"
tracing = { version = "0.1.40", default-features = false }
url = "2"
# Dev dependencies.
image = { version = "0.25.0", default-features = false }

View File

@@ -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"]

View File

@@ -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",

View File

@@ -1,4 +1,5 @@
use std::cell::{Cell, OnceCell, RefCell};
use std::collections::HashMap;
use std::mem;
use std::rc::Rc;
use std::sync::Arc;
@@ -6,22 +7,30 @@ use std::time::Instant;
use dispatch2::MainThreadBound;
use objc2::MainThreadMarker;
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSRunningApplication};
use objc2::rc::{Retained, Weak};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSDragOperation, NSRunningApplication,
};
use objc2_foundation::NSNotification;
use winit_common::core_foundation::{EventLoopProxy, MainRunLoop};
use winit_common::event_handler::EventHandler;
use winit_core::application::ApplicationHandler;
use winit_core::data_transfer::DataTransferId;
use winit_core::event::{StartCause, WindowEvent};
use winit_core::event_loop::ControlFlow;
use winit_core::event_loop::{ControlFlow, DndAction};
use winit_core::window::WindowId;
use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately};
use super::menu;
use super::observer::EventLoopWaker;
use crate::dnd::Pasteboards;
use crate::window_delegate::WindowDelegate;
#[derive(Debug)]
pub(super) struct AppState {
mtm: MainThreadMarker,
drag_state: RefCell<Option<DragState>>,
pasteboards: Pasteboards,
activation_policy: Option<NSApplicationActivationPolicy>,
default_menu: bool,
activate_ignoring_other_apps: bool,
@@ -43,10 +52,17 @@ pub(super) struct AppState {
start_time: Cell<Option<Instant>>,
wait_timeout: Cell<Option<Instant>>,
pending_redraw: RefCell<Vec<WindowId>>,
windows: RefCell<HashMap<WindowId, MainThreadBound<Weak<WindowDelegate>>>>,
// NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and
// as such should be careful to not add fields that, in turn, strongly reference those.
}
#[derive(Debug)]
pub(crate) struct DragState {
pub id: DataTransferId,
pub valid_actions: Vec<DndAction>,
}
// SAFETY: Creating `MainThreadBound` in a `const` context, where there is no concept of the
// main thread.
static GLOBAL: MainThreadBound<OnceCell<Rc<AppState>>> =
@@ -65,6 +81,8 @@ impl AppState {
let this = Rc::new(Self {
mtm,
pasteboards: Default::default(),
drag_state: Default::default(),
activation_policy,
default_menu,
activate_ignoring_other_apps,
@@ -82,6 +100,7 @@ impl AppState {
waker: RefCell::new(EventLoopWaker::new()),
start_time: Cell::new(None),
wait_timeout: Cell::new(None),
windows: Default::default(),
pending_redraw: RefCell::new(vec![]),
});
@@ -96,6 +115,20 @@ impl AppState {
.clone()
}
pub fn with_window_delegate_on_main<F, R>(&self, id: WindowId, func: F) -> Option<R>
where
F: FnOnce(Retained<WindowDelegate>) -> R + Send,
R: Send,
{
self.windows.borrow_mut().get(&id)?.get_on_main(move |delegate| delegate.load().map(func))
}
pub fn register_window(&self, window: &Retained<WindowDelegate>, 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<Self>, _notification: &NSNotification) {
@@ -371,6 +404,23 @@ impl AppState {
};
self.waker.borrow_mut().start_at(min_timeout(wait_timeout, app_timeout));
}
pub fn pasteboards(&self) -> &Pasteboards {
&self.pasteboards
}
pub fn drag_state(&self) -> &RefCell<Option<DragState>> {
&self.drag_state
}
pub(crate) fn proposed_drag_action(
&self,
source_operations: NSDragOperation,
) -> Option<DndAction> {
self.drag_state().borrow().as_ref().and_then(|drag_state| {
crate::dnd::preferred_drag_operation(source_operations, &drag_state.valid_actions)
})
}
}
/// Returns the minimum `Option<Instant>`, taking into account that `None`

View File

@@ -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<NSCursor>);
@@ -42,6 +43,40 @@ impl CustomCursor {
}
}
pub(crate) fn image_from_icon(icon: &Icon) -> Result<Retained<NSImage>, RequestError> {
let rgba_icon = icon
.cast_ref::<RgbaIcon>()
.ok_or(NotSupportedError::new("Only RGBA icons can be converted to `NSImage`"))?;
let width = rgba_icon.width();
let height = rgba_icon.height();
let bitmap = unsafe {
NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
NSBitmapImageRep::alloc(),
std::ptr::null_mut::<*mut c_uchar>(),
width as isize,
height as isize,
8,
4,
true,
false,
NSDeviceRGBColorSpace,
width as isize * 4,
32,
)
}.ok_or_else(|| os_error!("Initializing the `NSBitmapImageRep` failed"))?;
let bitmap_data =
unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), rgba_icon.buffer().len()) };
bitmap_data.copy_from_slice(rgba_icon.buffer());
let image = NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into()));
image.addRepresentation(&bitmap);
Ok(image)
}
pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Result<Retained<NSCursor>, RequestError> {
let width = cursor.width();
let height = cursor.height();

499
winit-appkit/src/dnd.rs Normal file
View File

@@ -0,0 +1,499 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::io;
use std::ops::{BitOr, ControlFlow};
use std::sync::{Arc, OnceLock};
use dispatch2::MainThreadBound;
use objc2::rc::{Retained, Weak};
use objc2::runtime::AnyObject;
use objc2::{AnyThread, DefinedClass as _, MainThreadMarker, Message, define_class, msg_send};
use objc2_app_kit::{
NSDragOperation, NSPasteboard, NSPasteboardType, NSPasteboardTypeFileURL, NSPasteboardTypeHTML,
NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF,
NSPasteboardWriting, NSPasteboardWritingOptions,
};
use objc2_foundation::{NSArray, NSData, NSObject, NSObjectProtocol, NSString};
use winit_core::data_transfer::{
DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData,
};
use winit_core::event_loop::DndAction;
use winit_core::window::WindowId;
/// A thin wrapper around [`NSPasteboardType`], implementing [`TransferType`].
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct PasteboardType {
hint: Option<TypeHint>,
// We need to convert `NSString` to `str` since `NSString` isn't `Send`/`Sync`
inner: Arc<str>,
}
impl PasteboardType {
fn from_hint(hint: TypeHint) -> Option<Self> {
let hint_to_pasteboard_type = unsafe {
[
(TypeHint::UriList, NSPasteboardTypeFileURL),
(TypeHint::Plaintext, NSPasteboardTypeString),
(TypeHint::Html, NSPasteboardTypeHTML),
(TypeHint::Image { extension_hint: Some("png") }, NSPasteboardTypePNG),
(TypeHint::Image { extension_hint: Some("tiff") }, NSPasteboardTypeTIFF),
(TypeHint::Audio { extension_hint: None }, NSPasteboardTypeSound),
]
};
hint_to_pasteboard_type.into_iter().find_map(|(haystack, inner)| {
(haystack.matches(&hint))
.then(|| Self { hint: Some(hint), inner: inner.to_string().into() })
})
}
}
impl From<Retained<NSPasteboardType>> for PasteboardType {
fn from(value: Retained<NSPasteboardType>) -> Self {
let pasteboard_type_to_hint = unsafe {
[
// Just in case the source application uses the deprecated method, we handle it
// here
#[expect(deprecated)]
(objc2_app_kit::NSFilenamesPboardType, TypeHint::UriList),
(NSPasteboardTypeFileURL, TypeHint::UriList),
(NSPasteboardTypeString, TypeHint::Plaintext),
(NSPasteboardTypeHTML, TypeHint::Html),
(NSPasteboardTypePNG, TypeHint::Image { extension_hint: Some("png") }),
(NSPasteboardTypeTIFF, TypeHint::Image { extension_hint: Some("tiff") }),
(NSPasteboardTypeSound, TypeHint::Audio { extension_hint: None }),
]
};
let hint = pasteboard_type_to_hint
.iter()
.find_map(|(pb_type, hint)| (**pb_type == *value).then_some(hint));
Self { hint: hint.copied(), inner: value.to_string().into() }
}
}
impl TransferType for PasteboardType {
fn hint(&self) -> Option<winit_core::data_transfer::TypeHint> {
self.hint
}
fn matches(&self, other: &dyn TransferType) -> bool {
if let Some(other_pb_type) = other.cast_ref::<Self>() {
*self == *other_pb_type
} else {
// If either hint is `None`, return false
self.hint().is_some_and(|hint| other.hint() == Some(hint))
}
}
}
/// A thin wrapper around [`NSPasteboard`], implementing [`DataTransfer`].
#[derive(Debug)]
pub struct Pasteboard {
transfer_id: DataTransferId,
ns_pasteboard: MainThreadBound<Retained<NSPasteboard>>,
types: OnceLock<Arc<[PasteboardType]>>,
}
impl Clone for Pasteboard {
fn clone(&self) -> Self {
let inner = self.ns_pasteboard.get_on_main(|inner| {
MainThreadBound::new(inner.clone(), MainThreadMarker::new().unwrap())
});
Self { transfer_id: self.transfer_id, ns_pasteboard: inner, types: self.types.clone() }
}
}
impl Pasteboard {
fn new(
transfer_id: DataTransferId,
ns_pasteboard: MainThreadBound<Retained<NSPasteboard>>,
) -> Self {
Self { transfer_id, ns_pasteboard, types: Default::default() }
}
/// Get the array of [`PasteboardType`]s advertized by this [`Pasteboard`].
pub fn types(&self) -> &[PasteboardType] {
self.types.get_or_init(|| {
self.ns_pasteboard.get_on_main(|pb| {
pb.types()
.map(|types| types.into_iter().map(PasteboardType::from).collect::<Vec<_>>())
.unwrap_or_default()
.into()
})
})
}
/// Get the `DataTransferId` of this pasteboard.
pub fn id(&self) -> DataTransferId {
self.transfer_id
}
/// Get a typed reader for this pasteboard. This is only necessary in the cross-platform case,
/// as a user downcasting to the platform-specific type can just access the `NSPasteboard`
/// directly.
pub(crate) fn with_type(&self, type_: PasteboardTypeSpec) -> PasteboardValue {
PasteboardValue { type_, pasteboard: self.clone() }
}
}
impl DataTransfer for Pasteboard {
fn for_each_available_type<'this>(
&'this self,
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>,
) {
let _ = self.types().iter().map(|mime| mime as &dyn TransferType).try_for_each(func);
}
}
#[derive(Debug, Clone)]
pub(crate) enum PasteboardTypeSpec {
PasteboardType(PasteboardType),
TypeHint(TypeHint),
}
impl PasteboardTypeSpec {
pub(crate) fn from_dyn(type_: &dyn TransferType) -> Option<Self> {
match type_.cast_ref::<PasteboardType>() {
Some(pb_type) => Some(Self::PasteboardType(pb_type.clone())),
None => type_.hint().map(Into::into),
}
}
}
impl From<TypeHint> for PasteboardTypeSpec {
fn from(value: TypeHint) -> Self {
match PasteboardType::from_hint(value) {
Some(pb_type) => Self::PasteboardType(pb_type),
None => Self::TypeHint(value),
}
}
}
impl PasteboardTypeSpec {
fn pasteboard_type(&self) -> Option<&PasteboardType> {
match self {
PasteboardTypeSpec::PasteboardType(pasteboard_type) => Some(pasteboard_type),
PasteboardTypeSpec::TypeHint(_) => None,
}
}
}
pub fn dnd_action_to_ns_drag_operation(value: DndAction) -> NSDragOperation {
match value {
DndAction::Copy => NSDragOperation::Copy,
DndAction::Move => NSDragOperation::Move,
DndAction::Link => NSDragOperation::Link,
DndAction::Private => NSDragOperation::Private,
_ => NSDragOperation::empty(),
}
}
pub fn ns_drag_operation_to_dnd_action(value: NSDragOperation) -> Option<DndAction> {
[
(NSDragOperation::Copy, DndAction::Copy),
(NSDragOperation::Move, DndAction::Move),
(NSDragOperation::Link, DndAction::Link),
(NSDragOperation::Private, DndAction::Private),
// Sometimes the OS returns `Generic`, in which case we just fall back to `Copy`.
(NSDragOperation::Generic, DndAction::Copy),
]
.into_iter()
.find_map(|(appkit, winit)| value.contains(appkit).then_some(winit))
}
pub fn dnd_actions_to_ns_drag_operation(value: &[DndAction]) -> NSDragOperation {
value
.iter()
.copied()
.map(dnd_action_to_ns_drag_operation)
.fold(NSDragOperation::empty(), BitOr::bitor)
}
pub fn preferred_drag_operation(
value: NSDragOperation,
preference: &[DndAction],
) -> Option<DndAction> {
preference
.iter()
.find(|action| value.intersects(dnd_action_to_ns_drag_operation(**action)))
.copied()
}
/// A thin wrapper around [`NSPasteboard`], implementing [`TypedData`].
#[derive(Debug)]
pub struct PasteboardValue {
// The concept of "top-level" types for a pasteboard doesn't always make sense on macOS due to
// the use of `pasteboardItems`, so we allow using `TypeHint` instead to preserve the user's
// intention.
type_: PasteboardTypeSpec,
pasteboard: Pasteboard,
}
impl TypedData for PasteboardValue {
fn type_(&self) -> &dyn TransferType {
match &self.type_ {
PasteboardTypeSpec::PasteboardType(pasteboard_type) => {
pasteboard_type as &dyn TransferType
},
PasteboardTypeSpec::TypeHint(type_hint) => type_hint,
}
}
fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
self.try_as_bytes()
.ok()
.map(|bytes| Box::new(io::Cursor::new(bytes)) as Box<dyn io::BufRead>)
}
fn try_as_bytes(&self) -> io::Result<Vec<u8>> {
let type_ = self.type_.clone();
self.pasteboard
.ns_pasteboard
.get_on_main(|pasteboard| {
let bytes =
pasteboard.dataForType(&NSString::from_str(&type_.pasteboard_type()?.inner))?;
Some(bytes.to_vec())
})
.ok_or_else(|| {
io::Error::other(format!(
"NSPasteboard doesn't advertise a binary representation for type {:?}",
self.type_
))
})
}
fn try_as_uris(&self) -> io::Result<Vec<String>> {
// TODO: We should probably use `readObjects`, need to check how that works.
if self.type_().hint() != Some(TypeHint::UriList) {
return Err(io::ErrorKind::InvalidData.into());
}
self.pasteboard.ns_pasteboard.get_on_main(|pasteboard| {
let Some(items) = pasteboard.pasteboardItems() else {
// The pasteboard didn't expose any items, so we try with the deprecated method.
#[expect(deprecated)]
let property_list = match pasteboard
.propertyListForType(unsafe { objc2_app_kit::NSFilenamesPboardType })
{
Some(property_list) => property_list,
None => {
return pasteboard
.stringForType(unsafe { NSPasteboardTypeFileURL })
.map(|ns_str| vec![ns_str.to_string()])
.ok_or_else(|| io::ErrorKind::InvalidData.into());
},
};
let paths = property_list
.downcast::<NSArray>()
.unwrap()
.into_iter()
.map(|file| file.downcast::<NSString>().unwrap().to_string())
.collect();
return Ok(paths);
};
Ok(items
.into_iter()
.filter_map(|item| item.stringForType(unsafe { NSPasteboardTypeFileURL }))
.map(|ns_str| ns_str.to_string())
.collect())
})
}
fn try_as_string(&self) -> io::Result<String> {
let type_ = self.type_.clone();
self.pasteboard.ns_pasteboard.get_on_main(|pasteboard| {
pasteboard
.stringForType(&NSString::from_str(
&type_.pasteboard_type().ok_or(io::ErrorKind::InvalidData)?.inner,
))
.map(|ns_str| ns_str.to_string())
.ok_or_else(|| io::ErrorKind::InvalidData.into())
})
}
}
#[derive(Debug)]
struct ActivePasteboard {
window_id: WindowId,
pb: MainThreadBound<Weak<NSPasteboard>>,
}
#[derive(Debug, Default)]
pub struct Pasteboards {
inner: RefCell<HashMap<DataTransferId, ActivePasteboard>>,
}
impl Pasteboards {
pub fn remove_deloaded_pasteboards(&self) {
self.inner.borrow_mut().retain(|_, ActivePasteboard { pb, .. }| {
pb.get_on_main(|state| state.load().is_some())
});
}
/// If the data transfer exists, update the pasteboard it points to.
pub fn set_pasteboard(
&self,
id: DataTransferId,
new_pb: &MainThreadBound<Retained<NSPasteboard>>,
) {
let mut inner = self.inner.borrow_mut();
if let Some(ActivePasteboard { pb, .. }) = inner.get_mut(&id) {
*pb = new_pb.get_on_main(|pb| {
MainThreadBound::new(Weak::from_retained(pb), MainThreadMarker::new().unwrap())
});
}
}
pub fn insert(
&self,
transfer_id: DataTransferId,
pb: &MainThreadBound<Retained<NSPasteboard>>,
window_id: WindowId,
) {
let mut inner = self.inner.borrow_mut();
let transfer = inner.entry(transfer_id).or_insert_with(move || {
pb.get_on_main(move |pb| ActivePasteboard {
window_id,
pb: MainThreadBound::new(Weak::from_retained(pb), MainThreadMarker::new().unwrap()),
})
});
transfer.window_id = window_id;
}
pub fn get(&self, id: DataTransferId) -> Option<Pasteboard> {
self.inner.borrow().get(&id).and_then(|ActivePasteboard { pb, .. }| {
pb.get_on_main(|state| {
let pb = state.load()?;
let pb = MainThreadBound::new(pb, MainThreadMarker::new().unwrap());
Some(Pasteboard::new(id, pb))
})
})
}
/// Get the window ID that most-recently saw the provided data transfer.
pub fn window_id(&self, id: DataTransferId) -> Option<WindowId> {
self.inner.borrow().get(&id).map(|active_pasteboard| active_pasteboard.window_id)
}
}
pub(crate) struct PasteboardWriterState {
data: Box<dyn DataTransferSend>,
// 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<Retained<NSString>>,
writable_types: Retained<NSArray<NSPasteboardType>>,
}
impl PasteboardWriter {
pub(crate) fn new(
value: Box<dyn DataTransferSend>,
uri: Option<Retained<NSString>>,
) -> Retained<Self> {
let mut writable_types = Vec::<Retained<NSPasteboardType>>::new();
value.for_each_available_type(&mut |type_| {
let Some(spec) = PasteboardTypeSpec::from_dyn(type_) else {
return ControlFlow::Continue(());
};
let Some(pb_type) = spec.pasteboard_type() else {
return ControlFlow::Continue(());
};
writable_types.push(NSString::from_str(&pb_type.inner));
ControlFlow::Continue(())
});
let pb_writer = Self::alloc().set_ivars(PasteboardWriterState {
data: value,
uri,
writable_types: NSArray::from_retained_slice(&writable_types),
});
// Unsure if there's an easier way to do this, but this is how `WindowDelegate` does it.
unsafe { msg_send![super(pb_writer), init] }
}
}
impl PasteboardWriterState {
fn data_for_pasteboard_type(
&self,
pasteboard_type: &NSPasteboardType,
) -> Option<Retained<AnyObject>> {
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<NSArray<NSPasteboardType>> {
let vars = self.ivars();
vars.writable_types.clone()
}
#[unsafe(method(writingOptionsForType:pasteboard:))]
fn writing_options_for_type(
&self,
type_: &NSPasteboardType,
pasteboard: &NSPasteboard,
) -> NSPasteboardWritingOptions {
let _ = type_;
let _ = pasteboard;
NSPasteboardWritingOptions::empty()
}
#[unsafe(method_id(pasteboardPropertyListForType:))]
fn pasteboard_property_list_for_type(
&self,
type_: &NSPasteboardType,
) -> Option<Retained<AnyObject>> {
let vars = self.ivars();
vars.data_for_pasteboard_type(type_)
}
}
);

View File

@@ -1,30 +1,38 @@
use std::fmt;
use std::rc::Rc;
use std::sync::Arc;
use std::time::{Duration, Instant};
use objc2::rc::{Retained, autoreleasepool};
use objc2::runtime::ProtocolObject;
use objc2::{MainThreadMarker, available};
use objc2::{AnyThread, MainThreadMarker, available};
use objc2_app_kit::{
NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification,
NSApplicationWillTerminateNotification, NSWindow,
NSApplicationWillTerminateNotification, NSDraggingItem, NSWindow,
};
use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopCommonModes};
use objc2_foundation::{NSNotificationCenter, NSObjectProtocol};
use objc2_core_foundation::{
CFIndex, CFRunLoopActivity, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes,
};
use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSString};
use rwh_06::HasDisplayHandle;
use tracing::debug_span;
use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers};
use winit_common::foundation::create_observer;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::data_transfer::{
DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint,
};
use winit_core::error::{EventLoopError, RequestError};
use winit_core::event::WindowEvent;
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
EventLoopProxy as CoreEventLoopProxy, OwnedDisplayHandle as CoreOwnedDisplayHandle,
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
DndAction, DragIcon, EventLoopProxy as CoreEventLoopProxy,
OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
use winit_core::window::Theme;
use winit_core::window::{Theme, WindowId};
use super::app::override_send_event;
use super::app_state::AppState;
@@ -32,6 +40,8 @@ use super::cursor::CustomCursor;
use super::event::dummy_event;
use super::monitor;
use crate::ActivationPolicy;
use crate::cursor::image_from_icon;
use crate::dnd::{PasteboardTypeSpec, PasteboardWriter, dnd_actions_to_ns_drag_operation};
use crate::window::Window;
#[derive(Debug)]
@@ -126,8 +136,183 @@ impl RootActiveEventLoop for ActiveEventLoop {
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
fn fetch_data_transfer(
&self,
id: DataTransferId,
type_: &dyn TransferType,
) -> Result<AsyncRequestSerial, RequestError> {
let Some(pb) = self.app_state.pasteboards().get(id) else {
return Err(RequestError::Ignored);
};
let Some(window_id) = self.app_state.pasteboards().window_id(id) else {
return Err(RequestError::Ignored);
};
let serial = AsyncRequestSerial::get();
let Some(type_) = PasteboardTypeSpec::from_dyn(type_) else {
return Err(os_error!(format!("Pasteboard does not contain type {type_:?}")).into());
};
let data = Arc::new(pb.with_type(type_));
self.app_state.maybe_queue_with_handler(move |app, event_loop| {
app.window_event(event_loop, window_id, WindowEvent::DataTransferReceived {
id,
serial,
value: data,
});
});
Ok(serial)
}
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
let Some(pb) = self.app_state.pasteboards().get(id) else {
return Err(RequestError::Ignored);
};
Ok(Box::new(pb))
}
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), RequestError> {
let mut state = self.app_state.drag_state().borrow_mut();
let Some(drag_state) = &mut *state else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
if drag_state.id != id {
return Err(os_error!(UnknownDataTransfer(id)).into());
}
drag_state.valid_actions.clear();
drag_state.valid_actions.extend_from_slice(actions);
Ok(())
}
fn start_drag(
&self,
source: WindowId,
send_data: Box<dyn DataTransferSend>,
actions: &[DndAction],
icon: Option<DragIcon>,
) -> Result<DataTransferId, RequestError> {
let drag_operation = dnd_actions_to_ns_drag_operation(actions);
self.app_state
.with_window_delegate_on_main(source, move |delegate| {
let (dragging_rect_offset_x, dragging_rect_offset_y) =
icon.as_ref().map(|icon| (icon.offset_x, icon.offset_y)).unwrap_or_default();
let drag_image = icon.and_then(|icon| image_from_icon(&icon.icon).ok());
let Some(event) = delegate.window().currentEvent() else {
return Err(RequestError::Ignored);
};
let dragging_rect_size = drag_image
.as_ref()
.map(|img| img.size())
// Seemingly we need some kind of dragging rectangle even if no icon is
// supplied.
.unwrap_or(CGSize::new(16., 16.));
let event_location = event.locationInWindow();
let dragging_rect_location = CGPoint::new(
event_location.x + dragging_rect_offset_x as f64,
// Convert generic coordinates (y=0 is top of image) to AppKit coordinates (y=0
// is bottom of image)
event_location.y - dragging_rect_size.height - dragging_rect_offset_y as f64,
);
let dragging_rect = CGRect::new(dragging_rect_location, dragging_rect_size);
let mut uris = send_data
.data_for_type(&TypeHint::UriList)
.and_then(|file_uris| {
// TODO: Might not be ideal to do this
let ns_url_from_str = |str: String| NSString::from_str(&str);
// Slightly complicated use of iterators in order to ensure that branches
// have the same opaque type
match file_uris {
SendData::Uris(os_strings) => Some(
None.into_iter().chain(os_strings.into_iter().map(ns_url_from_str)),
),
SendData::String(string) => Some(
Some(NSString::from_str(&string))
.into_iter()
.chain(Vec::new().into_iter().map(ns_url_from_str)),
),
SendData::Bytes(_) => None,
}
})
.into_iter()
.flatten();
let first_uri = uris.next();
let mut pasteboard_items = uris
.map(|ns_url| {
let dragging_item = NSDraggingItem::initWithPasteboardWriter(
NSDraggingItem::alloc(),
ProtocolObject::from_ref(&*ns_url),
);
// No dragging frame/contents, icon only applies to the first item.
dragging_item
})
.collect::<Vec<_>>();
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::DisplayHandle<'_>, rwh_06::HandleError> {
let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new());

View File

@@ -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};

View File

@@ -8,8 +8,9 @@ use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Sel};
use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send};
use objc2_app_kit::{
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSViewLayerContentsRedrawPolicy, NSWindow,
NSApplication, NSCursor, NSDragOperation, NSDraggingSession, NSEvent, NSEventPhase,
NSResponder, NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView,
NSViewLayerContentsRedrawPolicy, NSWindow,
};
use objc2_core_foundation::CGRect;
use objc2_foundation::{
@@ -114,6 +115,12 @@ pub struct ViewState {
/// Strong reference to the global application state.
app_state: Rc<AppState>,
/// This is for a dragging session that we initiated
dragging_session: RefCell<Option<Retained<NSDraggingSession>>>,
/// This is for a dragging session that we initiated
drag_operations: Cell<NSDragOperation>,
cursor_state: RefCell<CursorState>,
ime_position: Cell<NSPoint>,
ime_size: Cell<NSSize>,
@@ -789,6 +796,8 @@ impl WinitView {
) -> Retained<Self> {
let this = mtm.alloc().set_ivars(ViewState {
app_state: Rc::clone(app_state),
dragging_session: Default::default(),
drag_operations: Cell::new(NSDragOperation::empty()),
cursor_state: Default::default(),
ime_position: Default::default(),
ime_size: Default::default(),
@@ -898,7 +907,7 @@ impl WinitView {
}
}
fn scale_factor(&self) -> f64 {
pub(crate) fn scale_factor(&self) -> f64 {
self.window().backingScaleFactor() as f64
}
@@ -1117,6 +1126,29 @@ impl WinitView {
self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get()));
}
pub(crate) fn set_dragging_session(
&self,
drag: Retained<NSDraggingSession>,
action_mask: NSDragOperation,
) {
let vars = self.ivars();
vars.dragging_session.replace(Some(drag));
vars.drag_operations.set(action_mask);
}
pub(crate) fn drag_operations(&self) -> NSDragOperation {
self.ivars().drag_operations.get()
}
pub(crate) fn clear_dragging_session(&self, drag: &NSDraggingSession) -> bool {
let vars = self.ivars();
vars.drag_operations.set(NSDragOperation::empty());
let mut dragging_session = vars.dragging_session.borrow_mut();
dragging_session
.take_if(|session| session.draggingSequenceNumber() == drag.draggingSequenceNumber())
.is_some()
}
fn mouse_click(&self, event: &NSEvent, button_state: ElementState) {
let position = self.mouse_view_point(event).to_physical(self.scale_factor());
let button = mouse_button(event);

View File

@@ -36,6 +36,7 @@ impl Window {
let mtm = window_target.mtm;
let delegate =
autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?;
window_target.app_state.register_window(&delegate, mtm);
Ok(Window {
window: MainThreadBound::new(delegate.window().retain(), mtm),
delegate: MainThreadBound::new(delegate, mtm),

View File

@@ -6,6 +6,7 @@ use std::ptr;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use dispatch2::MainThreadBound;
use dpi::{
LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize,
Position, Size,
@@ -19,14 +20,14 @@ use objc2::{
use objc2_app_kit::{
NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization,
NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType,
NSColor, NSDraggingDestination, NSDraggingInfo, NSRequestUserAttentionType, NSScreen,
NSToolbar, NSView, NSViewFrameDidChangeNotification, NSWindow, NSWindowButton,
NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode,
NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility,
NSWindowToolbarStyle,
NSColor, NSDragOperation, NSDraggingContext, NSDraggingDestination, NSDraggingInfo,
NSDraggingSession, NSDraggingSource, NSPasteboardTypeFileURL, NSPasteboardTypeHTML,
NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF,
NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification,
NSWindow, NSWindowButton, NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState,
NSWindowOrderingMode, NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode,
NSWindowTitleVisibility, NSWindowToolbarStyle,
};
#[allow(deprecated)]
use objc2_app_kit::{NSFilenamesPboardType, NSWindowFullScreenButton};
use objc2_core_foundation::{CGFloat, CGPoint};
use objc2_core_graphics::{
CGAcquireDisplayFadeReservation, CGAssociateMouseAndMouseCursorPosition, CGDisplayCapture,
@@ -44,6 +45,7 @@ use objc2_foundation::{
use tracing::{debug_span, trace, warn};
use winit_common::core_foundation::MainRunLoop;
use winit_core::cursor::Cursor;
use winit_core::data_transfer::DataTransferId;
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::event::{SurfaceSizeWriter, WindowEvent};
use winit_core::icon::Icon;
@@ -60,6 +62,10 @@ use super::monitor::{self, MonitorHandle, flip_window_screen_coordinates, get_di
use super::util::cgerr;
use super::view::WinitView;
use super::window::{WinitPanel, WinitWindow, window_id};
use crate::app_state::DragState;
use crate::dnd::{
dnd_action_to_ns_drag_operation, ns_drag_operation_to_dnd_action, preferred_drag_operation,
};
use crate::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS};
#[derive(Debug)]
@@ -358,37 +364,78 @@ define_class!(
}
}
unsafe impl NSDraggingSource for WindowDelegate {
#[unsafe(method(draggingSession:sourceOperationMaskForDraggingContext:))]
fn dragging_session_source_operation_mask(
&self,
_: &NSDraggingSession,
_: NSDraggingContext,
) -> NSDragOperation {
self.view().drag_operations()
}
#[unsafe(method(draggingSession:endedAtPoint:operation:))]
fn dragging_session_ended_at_point(
&self,
session: &NSDraggingSession,
_: NSPoint,
operation: NSDragOperation,
) {
let id = DataTransferId::from_raw(session.draggingSequenceNumber() as i64);
if operation == NSDragOperation::None {
self.queue_event(WindowEvent::OutgoingDragCanceled { id });
} else {
self.queue_event(WindowEvent::OutgoingDragDropped {
id,
action: ns_drag_operation_to_dnd_action(operation),
});
}
self.view().clear_dragging_session(session);
}
}
unsafe impl NSDraggingDestination for WindowDelegate {
/// Invoked when the dragged image enters destination bounds or frame
#[unsafe(method(draggingEntered:))]
fn dragging_entered(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
fn dragging_entered(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> 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::<NSArray>()
.unwrap()
.into_iter()
.map(|file| PathBuf::from(file.downcast::<NSString>().unwrap().to_string()))
.collect();
let pb =
MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap());
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
let position =
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
self.queue_event(WindowEvent::DragEntered { paths, position });
let window_id = self.id();
true
let vars = self.ivars();
let source_operations = sender.draggingSourceOperationMask();
let transfer_id = DataTransferId::from_raw(sender.draggingSequenceNumber() as i64);
vars.app_state.pasteboards().insert(transfer_id, &pb, window_id);
vars.app_state
.drag_state()
.replace(Some(DragState { id: transfer_id, valid_actions: vec![] }));
self.queue_event(WindowEvent::DragEntered {
id: transfer_id,
position: Some(position),
});
let drag_state = vars.app_state.drag_state().borrow();
drag_state
.as_ref()
.and_then(|drag_state| {
preferred_drag_operation(source_operations, &drag_state.valid_actions)
})
.map(dnd_action_to_ns_drag_operation)
.unwrap_or(NSDragOperation::empty())
}
#[unsafe(method(wantsPeriodicDraggingUpdates))]
@@ -400,17 +447,46 @@ define_class!(
/// Invoked periodically as the image is held within the destination area, allowing
/// modification of the dragging operation or mouse-pointer position.
#[unsafe(method(draggingUpdated:))]
fn dragging_updated(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
fn dragging_updated(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> NSDragOperation {
let _entered = debug_span!("draggingUpdated:").entered();
let vars = self.ivars();
let Some(transfer_id) =
vars.app_state.drag_state().borrow().as_ref().map(|state| state.id)
else {
return NSDragOperation::empty();
};
let pb =
MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap());
let source_operations = sender.draggingSourceOperationMask();
vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb);
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
let position =
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
self.queue_event(WindowEvent::DragMoved { position });
let proposed_action = vars.app_state.proposed_drag_action(source_operations);
true
self.queue_event(WindowEvent::DragPosition {
id: transfer_id,
position,
proposed_action,
});
let drag_state = vars.app_state.drag_state().borrow();
drag_state
.as_ref()
.and_then(|drag_state| {
preferred_drag_operation(source_operations, &drag_state.valid_actions)
})
.map(dnd_action_to_ns_drag_operation)
.unwrap_or(NSDragOperation::empty())
}
/// Invoked when the image is released
@@ -425,30 +501,43 @@ define_class!(
fn perform_drag_operation(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
let _entered = debug_span!("performDragOperation:").entered();
use std::path::PathBuf;
let vars = self.ivars();
let pb = sender.draggingPasteboard();
#[allow(deprecated)]
let property_list = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) {
Some(property_list) => property_list,
None => return false.into(),
let Some(transfer_id) =
vars.app_state.drag_state().borrow().as_ref().map(|state| state.id)
else {
return false.into();
};
let paths = property_list
.downcast::<NSArray>()
.unwrap()
.into_iter()
.map(|file| PathBuf::from(file.downcast::<NSString>().unwrap().to_string()))
.collect();
let pb =
MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap());
let source_operations = sender.draggingSourceOperationMask();
// let operations = ns_drag_operation_to_dnd_actions(source_operations);
vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb);
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
let position =
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
self.queue_event(WindowEvent::DragDropped { paths, position });
let proposed_action = vars.app_state.proposed_drag_action(source_operations);
self.queue_event(WindowEvent::DragPosition {
id: transfer_id,
position,
proposed_action,
});
// Check again, in case the application updated
let proposed_action = vars.app_state.proposed_drag_action(source_operations);
self.queue_event(WindowEvent::DragDropped { id: transfer_id, proposed_action });
// We assume that if the OS has sent `perform_drag_operation`, that the drag succeeded.
// We may want to extend this API in the future to allow signalling that the final drop
// failed.
true
}
@@ -456,6 +545,10 @@ define_class!(
#[unsafe(method(concludeDragOperation:))]
fn conclude_drag_operation(&self, _sender: Option<&NSObject>) {
let _entered = debug_span!("concludeDragOperation:").entered();
let vars = self.ivars();
vars.app_state.pasteboards().remove_deloaded_pasteboards();
vars.app_state.drag_state().take();
}
/// Invoked when the dragging operation is cancelled
@@ -463,13 +556,39 @@ define_class!(
fn dragging_exited(&self, sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) {
let _entered = debug_span!("draggingExited:").entered();
let position = sender.map(|sender| {
let vars = self.ivars();
let Some(transfer_id) =
vars.app_state.drag_state().borrow().as_ref().map(|state| state.id)
else {
return;
};
if let Some(sender) = sender {
let pb = MainThreadBound::new(
sender.draggingPasteboard(),
MainThreadMarker::new().unwrap(),
);
vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb);
let dl = sender.draggingLocation();
let dl = self.view().convertPoint_fromView(dl, None);
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor())
});
let position =
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
self.queue_event(WindowEvent::DragLeft { position });
let source_operations = sender.draggingSourceOperationMask();
let proposed_action = vars.app_state.proposed_drag_action(source_operations);
self.queue_event(WindowEvent::DragPosition {
id: transfer_id,
position,
proposed_action,
});
}
self.queue_event(WindowEvent::DragLeft { id: transfer_id });
vars.app_state.drag_state().take();
}
}
@@ -674,7 +793,7 @@ fn new_window(
if macos_attrs.titlebar_buttons_hidden {
for titlebar_button in &[
#[allow(deprecated)]
NSWindowFullScreenButton,
objc2_app_kit::NSWindowFullScreenButton,
NSWindowButton::MiniaturizeButton,
NSWindowButton::CloseButton,
NSWindowButton::ZoomButton,
@@ -756,10 +875,6 @@ fn new_window(
window.setBackgroundColor(Some(&NSColor::clearColor()));
}
// register for drag and drop operations.
#[allow(deprecated)]
window.registerForDraggedTypes(&NSArray::from_slice(&[unsafe { NSFilenamesPboardType }]));
Some(window)
})
}
@@ -837,6 +952,22 @@ impl WindowDelegate {
window.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
let drag_types = unsafe {
// Advertize support for the set of types which correspond to variants of `TypeHint`.
// If the user wants to support other pasteboard types which don't have a cross-platform
// equivalent, they can downcast the window and manually call `registerForDraggedTypes`
// themselves.
NSArray::from_slice(&[
NSPasteboardTypeFileURL,
NSPasteboardTypeHTML,
NSPasteboardTypePNG,
NSPasteboardTypeSound,
NSPasteboardTypeString,
NSPasteboardTypeTIFF,
])
};
window.registerForDraggedTypes(&drag_types);
// Listen for theme change event.
//
// SAFETY: The observer is un-registered in the `Drop` of the delegate.

View File

@@ -30,6 +30,7 @@ keyboard-types.workspace = true
rwh_06.workspace = true
serde = { workspace = true, optional = true }
smol_str.workspace = true
url.workspace = true
# `wasm32-unknown-unknown` and `wasm32-none`, but not `wasm32-wasi`.
[target.'cfg(all(target_family = "wasm", any(target_os = "unknown", target_os = "none")))'.dependencies]

View File

@@ -0,0 +1,593 @@
//! Cross-platform abstractions related to data transfer (i.e. clipboard and drag-and-drop).
//!
//! > **NOTE**: Interacting with the clipboard is currently not implemented in Winit, and
//! > this API is only used for drag-and-drop.
//!
//! # Quickstart
//!
//! The API in this module is used for both sending and receiving data. The flow is detailed below,
//! but to quickly get started, the relevant APIs are the following:
//!
//! ### Receiving a drag-and-drop operation
//!
//! - [`DragEntered`](crate::event::WindowEvent::DragEntered) - informs a window that a new drag
//! operation has started.
//! - [`data_transfer`](crate::event_loop::ActiveEventLoop::data_transfer) - get metadata about the
//! incoming transfer.
//! - [`DataTransfer`] - metadata about the incoming transfer, in particular the available types
//! - [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions) - the
//! application must set at least some actions as valid in order for the drag to be considered
//! accepted.
//! - [`fetch_data_transfer`](crate::event_loop::ActiveEventLoop::fetch_data_transfer) - request the
//! actual data, with a specific type, from the data transfer.
//! - [`DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) - the actual data,
//! with a specific type, has been received.
//! - [`TypedData`] - provides methods to read the actual data
//!
//! ### Sending a drag-and-drop operation
//!
//! - [`DataTransferSend`] - the core trait which defines data to be sent
//! - [`DataTransferSendBuilder`] - helper to create a new outgoing data transfer from a set of
//! types and callbacks that supply data of that type
//! - [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag) - the
//! application calls this to start a new drag operation
//! - [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped)/
//! [`OutgoingDragCanceled`](crate::event::WindowEvent::OutgoingDragCanceled) - the application
//! receives this when the user has ended the drag operation, by dropping the data or by canceling
//! the operation respectively
//!
//! # Detailed flow
//!
//! ## Receiving a drag-and-drop operation
//!
//! On all platforms, the process looks something like this:
//!
//! - A data transfer advertises a set of types which the data can be interpreted as. While the
//! precise implementation depends on platform, there's a set of types which can be safely
//! transferred between applications on all platforms (see [`TypeHint`]).
//! - For example, if you copy or drag text from a web page, the browser may advertise the text
//! formatted using HTML, the text formatted as RTF, and the text with all formatting removed
//! simultaneously.
//! - An application receiving a data transfer chooses one or more types that it understands and
//! requests the data in those formats (in practice, it will usually only request a single
//! format).
//! - The source application converts the data stored in its memory to the requested format and
//! asynchronously sends it to the target application
//!
//! On some platforms, the data is sometimes available synchronously, but all platforms have at
//! least some method of sending the data asynchronously and some types of data that may _only_ be
//! sent using the asynchronous interface. Because of this, the API in winit must be asynchronous.
//!
//! The flow for a user application that implements drag-and-drop would look something like this:
//!
//! - The application receives a [`DragEntered`](crate::event::WindowEvent::DragEntered) event. This
//! event supplies a [`DataTransferId`] which can be used to request information or operations on
//! the dragged data by using methods on [`Window`](crate::window::Window).
//! - To make sure that the operating system displays the correct cursor, and that modifier keys
//! will change the selected drag action correctly, the application should call
//! [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions). See
//! documentation on that method for details.
//! - As the drag operation continues, the window will receive
//! [`DragPosition`](crate::event::WindowEvent::DragPosition) events.
//! - At any point during this operation, the receiving application may request either the available
//! types or even the data being transferred. This may be useful in cases where the application
//! wants to preload the data. For example, an image editor may want to display the image on the
//! canvas during the drag operation.
//! - When the user tries to drop the data onto the window, that window will receive either a
//! [`DragDropped`](crate::event::WindowEvent::DragDropped) or
//! [`DragLeft`](crate::event::WindowEvent::DragLeft) event if the drag operation was accepted or
//! rejected, respectively. See documentation for
//! [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions) for
//! details on accepting/rejecting a drag.
//!
//! ## Sending a drag-and-drop operation
//!
//! As the source application cannot interact with the ongoing drag while it is in-flight, this flow
//! is a lot simpler.
//!
//! - The application creates a [`DataTransferSend`] with a set of types and associated data. For
//! most cases, this can be done with [`DataTransferSendBuilder`].
//! - The application passes this [`DataTransferSend`] to
//! [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag)`. This is also
//! where metadata is set, such as the icon that will be shown during the drag operation.
//! - When the drag operation completes, the application receives
//! [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped) with the resultant
//! action, or [`OutgoingDragCanceled`](crate::event::WindowEvent::OutgoingDragCanceled), and
//! handles it appropriately. For example, if the drag was successful and the operation is
//! [`DndAction::Move`](crate::event_loop::DndAction::Move), then the application would delete the
//! source object, since the data has now been transferred somewhere else.
#![warn(missing_docs)]
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use std::{fmt, io};
use crate::as_any::AsAny;
/// Unique identifier for a data transfer.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DataTransferId(i64);
impl DataTransferId {
/// Convert the [`DataTransferId`] into the underlying integer.
///
/// This is useful if you need to pass the ID across an FFI boundary, or store it in an atomic.
pub const fn into_raw(self) -> i64 {
self.0
}
/// Construct a [`DataTransferId`] from the underlying integer.
///
/// This should only be called with integers returned from [`DataTransferId::into_raw`].
pub const fn from_raw(id: i64) -> Self {
Self(id)
}
}
/// The set of types supported cross-platform.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum TypeHint {
/// Plain UTF-8 text (see [`TypedData::try_as_string`]).
///
/// **Note for platform implementations**: this hint is _only_ for UTF-8 text. If the platform
/// returns plaintext in some format other than UTF-8 by default, a [`TypedData`]
/// implementation marked with this type hint should convert to UTF-8.
Plaintext,
/// A list of URIs in the format defined by the `text/uri-list` MIME type, encoded as UTF-8 (see
/// [`TypedData::try_as_uris`]).
///
/// **Note for platform implementations**: this hint is _only_ for URIs encoded precisely in the
/// format specified above. If the platform uses a different format, a [`TypedData`]
/// implementation marked with this type hint should convert to that format.
UriList,
/// A HTML-formatted string
Html,
/// An RTF-formatted string
Rtf,
/// Audio
Audio {
/// An optional hint for the encoding of the supplied bytes, specified using the standard
/// file extension for that audio format, lowercase and without the leading `.`.
extension_hint: Option<&'static str>,
},
/// Image data
Image {
/// An optional hint for the encoding of the supplied bytes, specified using the standard
/// file extension for that image format, lowercase and without the leading `.`.
extension_hint: Option<&'static str>,
},
}
impl TypeHint {
/// Check whether the two type hints "match".
///
/// This is subtly different to direct equality. If one of the types is an image or audio with a
/// `None` extension hint, then the other type just needs to match variant (i.e. image/audio),
/// the extension does not also have to be `None`.
pub fn matches(&self, other: &Self) -> bool {
match (self, other) {
(Self::Plaintext, Self::Plaintext)
| (Self::UriList, Self::UriList)
| (Self::Html, Self::Html)
| (Self::Rtf, Self::Rtf) => true,
(
Self::Audio { extension_hint: this_ext },
Self::Audio { extension_hint: other_ext },
)
| (
Self::Image { extension_hint: this_ext },
Self::Image { extension_hint: other_ext },
) => match (this_ext, other_ext) {
(Some(this_ext), Some(other_ext)) => this_ext == other_ext,
(None, _) | (_, None) => true,
},
_ => false,
}
}
}
/// The type of a data transfer.
///
/// [`hint`](TransferType::hint) can be called to get the type in
/// a cross-platform format (see [`TypeHint`])
pub trait TransferType: AsAny + fmt::Debug {
/// Get the cross-platform representation of this type.
///
/// If this returns `None`, then this is a platform-dependent type that has no cross-platform
/// equivalent.
fn hint(&self) -> Option<TypeHint>;
/// 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<TypeHint> {
Some(*self)
}
fn matches(&self, other: &dyn TransferType) -> bool {
other.hint().is_some_and(|hint| self.matches(&hint))
}
}
impl_dyn_casting!(TransferType);
// Replicates the cfg for `url::Url::parse`
#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit"))]
fn default_try_as_file_paths<T: TypedData + ?Sized>(data: &T) -> io::Result<Vec<PathBuf>> {
data.try_as_uris().and_then(|uris| {
uris.into_iter()
.map(|uri_string| {
Ok(url::Url::parse(&uri_string)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
.to_file_path()
.map_err(|()| io::ErrorKind::InvalidData)?)
})
.collect()
})
}
// Replicates the cfg for `url::Url::parse`
//
// It doesn't matter that this is unimplemented on the web, as we don't currently support
// drag-and-drop for web targets and the web platform can't directly access paths anyway.
#[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit")))]
fn default_try_as_file_paths<T: TypedData + ?Sized>(_: &T) -> io::Result<Vec<PathBuf>> {
Err(io::ErrorKind::Unsupported.into())
}
/// Data that has been fetched from a data transfer
///
/// ### Blocking
///
/// Note that this type provides a blocking interface. In cases where reading this type directly on
/// the event loop would cause a deadlock, the backend will make a best-effort attempt to return an
/// error with [`io::ErrorKind::Deadlock`]. For now, the only way to access the data is via blocking
/// on the event loop, so simply retrying the next time an event is received that references the
/// data transfer should be enough to ensure that the data is accessible.
pub trait TypedData: AsAny + fmt::Debug + Send + Sync {
/// The type of this `TypedData`.
fn type_(&self) -> &dyn TransferType;
/// If this value is readable as bytes, return a reader than can be used to read those bytes.
///
/// On some platforms, the reader must be driven incrementally upon each
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)`. If
/// you don't need to stream the data and just want the bytes in a single buffer, use
/// [`TypedData::try_as_bytes`].
fn try_read(&self) -> Option<Box<dyn io::BufRead>>;
/// If this value is readable as bytes, return those bytes.
///
/// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called
/// again upon next receiving
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)
fn try_as_bytes(&self) -> io::Result<Vec<u8>> {
let mut reader = self
.try_read()
.ok_or_else(|| io::Error::other("This `TypedData` is not readable as bytes"))?;
let mut out = Vec::new();
reader.read_to_end(&mut out)?;
Ok(out)
}
/// Read this value as a list of URIs.
///
/// If this value is not readable as URIs, return an error.
///
/// The returned `String`s should be interpreted as URIs conforming to [RFC 3986](https://www.rfc-editor.org/info/rfc3986/).
///
/// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called
/// again upon next receiving
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)
fn try_as_uris(&self) -> io::Result<Vec<String>>;
/// Read this value as a list of paths.
///
/// This is provided as a convenience method to avoid the need for the user to manually parse
/// the result of [`try_as_uris`](TypedData::try_as_uris). `try_as_uris` should be preferred
/// when the extra complexity is acceptable, as it is more generic.
///
/// If this value is not readable as URIs, return an error.
///
/// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called
/// again upon next receiving
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)
fn try_as_file_paths(&self) -> io::Result<Vec<PathBuf>> {
default_try_as_file_paths(self)
}
/// Read this value as a plain text string.
///
/// If this value is not readable as a string, return an error.
///
/// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called
/// again upon next receiving
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)
fn try_as_string(&self) -> io::Result<String>;
}
// Required for `WindowEvent` to implement `PartialEq` - we just implement this on a best-effort
// basis.
impl PartialEq for dyn TypedData {
fn eq(&self, other: &Self) -> bool {
std::ptr::addr_eq(self, other)
}
}
impl_dyn_casting!(TypedData);
/// Metadata about a data transfer. This does not allow actually receiving data, as that is an
/// asynchronous operation. To fetch the data from the source application, see
/// [`ActiveEventLoop::fetch_data_transfer`](crate::event_loop::ActiveEventLoop::fetch_data_transfer).
pub trait DataTransfer: AsAny + fmt::Debug {
/// Iterate over each type advertized by this `DataTransfer`. This is just a minor optimization,
/// in most cases you should probably use [`has_type`](DataTransfer::has_type) or
/// [`available_types`](DataTransfer::available_types).
fn for_each_available_type<'this>(
&'this self,
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>,
);
/// Display the list of all available types.
///
/// This is useful if more-complex type matching is required, but for most cases
/// [`has_type`](DataTransfer::has_type) should be used.
fn available_types(&self) -> Vec<&'_ dyn TransferType> {
let mut out = Vec::new();
self.for_each_available_type(&mut |ty| {
out.push(ty);
ControlFlow::Continue(())
});
out
}
/// Check if the supplied type is provided by this [`DataTransfer`].
///
/// Supplying a [`TypeHint`] as the type is supported on all platforms, but if some
/// platform-specific type is required then that platform's implementation of `TransferType` can
/// be used.
fn has_type(&self, type_: &dyn TransferType) -> bool {
let mut found = false;
self.for_each_available_type(&mut |haystack| {
if haystack.matches(type_) {
found = true;
ControlFlow::Break(())
} else {
ControlFlow::Continue(())
}
});
found
}
}
impl_dyn_casting!(DataTransfer);
/// Kinds of data that can be sent via a `DataTransfer`.
///
/// Some kinds of data cannot be represented by just a binary blob in a cross-platform way.
/// File URIs on Windows and macOS are represented as arrays of strings, and strings have
/// different encoding on different platforms. To allow this to be represented, we allow
/// supplying strings and URIs separately from binary blobs.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SendData {
/// List of URIs.
///
/// These should conform to [RFC 3986](https://www.rfc-editor.org/info/rfc3986/).
/// If you just want to send file paths, see [`SendData::from_file_paths`].
///
/// Note that `SendData` implements `From<String>` and `From<Vec<u8>>`, but _not_
/// `From<Vec<String>>`, as it is not necessarily obvious to a reader that `Vec<String>`
/// will be interpreted as a URI list. However, it _does_ implement [`From<Url>`](url::Url),
/// if you are using the [`url`](https://docs.rs/url/2) crate.
Uris(Vec<String>),
/// String
///
/// This can also be constructed with the [`From<String>`](std::string::String) implementation.
String(String),
/// Binary blob
///
/// This can also be constructed with the [`From<Vec<u8>>`](std::vec::Vec) implementation.
Bytes(Vec<u8>),
}
impl SendData {
/// Create [`SendData::Uris`] from an iterator of [`Path`]s.
///
/// All paths must be absolute, and on Windows must include either a drive prefix (e.g. `C:\`)
/// or a UNC prefix (`\\`). See documentation for [`url::Url::from_file_path`].
pub fn from_file_paths<I>(paths: I) -> Option<Self>
where
I: IntoIterator,
I::Item: AsRef<Path>,
{
// Replicates the cfg for `url::Url::from_file_path`
#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit"))]
fn from_file_paths_impl<I>(paths: I) -> Option<SendData>
where
I: IntoIterator,
I::Item: AsRef<Path>,
{
paths
.into_iter()
.map(url::Url::from_file_path)
.map(|result| result.map(String::from))
.collect::<Result<Vec<_>, ()>>()
.map(SendData::Uris)
.ok()
}
// Replicates the cfg for `url::Url::from_file_path`
//
// It doesn't matter that this is unimplemented on the web, as we don't currently support
// drag-and-drop for web targets and the web platform can't directly access paths
// anyway.
#[cfg(not(any(
unix,
windows,
target_os = "redox",
target_os = "wasi",
target_os = "hermit"
)))]
fn from_file_paths_impl<I>(_: I) -> Option<SendData> {
None
}
from_file_paths_impl(paths)
}
}
// We monomorphize these `From` implementations instead of making them generic, in order to
// prevent accidentally casting to the wrong type.
impl From<String> for SendData {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<Vec<u8>> for SendData {
fn from(value: Vec<u8>) -> Self {
Self::Bytes(value)
}
}
impl From<Vec<url::Url>> for SendData {
fn from(value: Vec<url::Url>) -> Self {
Self::Uris(value.into_iter().map(Into::into).collect())
}
}
/// Trait for sending data via a data transfer.
///
/// See [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag) for where
/// this is used. To build an implementation of this trait dynamically in a cross-platform way, use
/// [`DataTransferSendBuilder`].
pub trait DataTransferSend: DataTransfer + Send {
/// Get the data for the specified type, or `None` if this value does not supply the given data
/// type.
fn data_for_type(&self, type_: &dyn TransferType) -> Option<SendData>;
}
impl_dyn_casting!(DataTransferSend);
type SendDataCallback<T> = Box<dyn Fn(&T, &dyn TransferType) -> Option<SendData> + 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<T> {
state: T,
types: Vec<(Box<dyn TransferType + Send>, SendDataCallback<T>)>,
}
impl<T> fmt::Debug for DataTransferSendBuilder<T>
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<T> DataTransfer for DataTransferSendBuilder<T>
where
T: fmt::Debug + Send + 'static,
{
fn for_each_available_type<'this>(
&'this self,
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>,
) {
let _ = self.types.iter().try_for_each(|(ty, _)| func(&**ty));
}
}
impl<T> DataTransferSend for DataTransferSendBuilder<T>
where
T: fmt::Debug + Send + 'static,
{
fn data_for_type(&self, type_: &dyn TransferType) -> Option<SendData> {
self.data_for_type(type_)
}
}
impl<T> DataTransferSendBuilder<T> {
/// Create a new [`DataTransferSendBuilder`], with a state value which acts as
/// the single source of truth for the underlying data.
pub fn new(state: T) -> Self {
Self { state, types: vec![] }
}
}
impl<T> DataTransferSendBuilder<T> {
fn data_for_type(&self, type_: &dyn TransferType) -> Option<SendData> {
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<Ty, F, O>(&mut self, type_: Ty, func: F) -> &mut Self
where
Ty: TransferType + Send,
F: Fn(&T, &dyn TransferType) -> Option<O> + Send + 'static,
O: Into<SendData>,
{
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<Ty, F, O>(mut self, type_: Ty, func: F) -> Self
where
Ty: TransferType + Send,
F: Fn(&T, &dyn TransferType) -> Option<O> + Send + 'static,
O: Into<SendData>,
{
self.add_type(type_, func);
self
}
}
impl<T> DataTransferSendBuilder<T>
where
T: fmt::Debug + Send + 'static,
{
/// Consume the builder, returning an implementation of [`DataTransferSend`].
///
/// Note that this is only provided for explicitness and ergonomics. [`DataTransferSendBuilder`]
/// implements [`DataTransferSend`] and this method is equivalent to [`Box::new`].
pub fn build(self) -> Box<dyn DataTransferSend> {
Box::new(self)
}
}

View File

@@ -2,8 +2,7 @@
use std::cell::LazyCell;
use std::cmp::Ordering;
use std::f64;
use std::path::PathBuf;
use std::sync::{Mutex, Weak};
use std::sync::{Arc, Mutex, Weak};
use dpi::{PhysicalPosition, PhysicalSize};
#[cfg(feature = "serde")]
@@ -11,8 +10,9 @@ use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use crate::Instant;
use crate::data_transfer::{DataTransferId, TypedData};
use crate::error::RequestError;
use crate::event_loop::AsyncRequestSerial;
use crate::event_loop::{AsyncRequestSerial, DndAction};
use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState};
#[cfg(doc)]
use crate::window::Window;
@@ -75,42 +75,104 @@ pub enum WindowEvent {
/// The window has been destroyed.
Destroyed,
/// A file drag operation has entered the window.
/// A drag operation has entered the window.
///
/// The user can use the `id` to read information about the incoming dragged data, and report
/// whether the operation is accepted or rejected back to the operating system (see
/// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`](`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`)).
///
/// To read the data being dragged, see
/// [`crate::event_loop::ActiveEventLoop::fetch_data_transfer`](`crate::event_loop::ActiveEventLoop::fetch_data_transfer`).
DragEntered {
/// List of paths that are being dragged onto the window.
paths: Vec<PathBuf>,
/// (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<f64>,
},
/// A file drag operation has moved over the window.
DragMoved {
/// (x,y) coordinates in pixels relative to the top-left corner of the window. May be
/// negative on some platforms if something is dragged over a window's decorations (title
/// bar, frame, etc).
position: PhysicalPosition<f64>,
},
/// The file drag operation has dropped file(s) on the window.
DragDropped {
/// List of paths that are being dragged onto the window.
paths: Vec<PathBuf>,
/// (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<f64>,
},
/// The file drag operation has been cancelled or left the window.
DragLeft {
/// (x,y) coordinates in pixels relative to the top-left corner of the window. May be
/// negative on some platforms if something is dragged over a window's decorations (title
/// bar, frame, etc).
/// ID of the data transfer object, see
/// [`crate::event_loop::ActiveEventLoop::data_transfer`](`crate::event_loop::ActiveEventLoop::data_transfer`).
id: DataTransferId,
/// (x,y) coordinates in pixels relative to the top-left corner of the window.
///
/// ## Platform-specific
/// May be negative on some platforms if something is dragged over a window's decorations
/// (title bar, frame, etc).
///
/// - **Windows:** Always emits [`None`].
/// Some platforms will provide this on enter, others do not. If
/// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`](`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`)
/// is never called, the default state is for the drag operation to be rejected. The
/// position is provided here when available to allow the application to accept a drag
/// operation as soon as possible, preventing the cursor from flickering from rejected to
/// accepted.
position: Option<PhysicalPosition<f64>>,
},
/// The position of an ongoing drag operation has changed.
DragPosition {
/// ID of the data transfer object, see
/// [`crate::event_loop::ActiveEventLoop::data_transfer`](`crate::event_loop::ActiveEventLoop::data_transfer`).
id: DataTransferId,
/// (x,y) coordinates in pixels relative to the top-left corner of the window.
///
/// May be negative on some platforms if something is dragged over a window's decorations
/// (title bar, frame, etc).
position: PhysicalPosition<f64>,
/// The drag action proposed by the OS, based on the actions supplied in
/// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`], the actions available on
/// the source, and the held modifier keys.
///
/// This may be `None` if the backend has not supplied a valid action. On some platforms
/// (in particular, X11), the application is only informed of the proposed action once
/// the operation completes.
proposed_action: Option<DndAction>,
},
/// A drag operation has dropped file(s) on the window.
DragDropped {
/// ID of the data transfer object, see
/// [`crate::event_loop::ActiveEventLoop::data_transfer`].
id: DataTransferId,
/// The drag action proposed by the OS, based on the actions supplied in
/// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`], the actions available on
/// the source, and the held modifier keys.
///
/// This may be `None` if the backend has not supplied a valid action. This is different
/// from the drag being canceled: the drag completed successfully, we just don't know
/// what action was selected.
proposed_action: Option<DndAction>,
},
/// A drag operation has been canceled or left the window.
DragLeft {
/// ID of the data transfer object, see
/// [`crate::event_loop::ActiveEventLoop::data_transfer`].
id: DataTransferId,
},
/// Data is available for a specific fetch request, see
/// [`fetch_data_transfer`](crate::event_loop::ActiveEventLoop::data_transfer).
///
/// While winit makes a best effort to only send this event precisely once, on some platforms it
/// may not be possible to uniquely determine the window that should receive it. In these
/// cases, winit may dispatch the event to all windows that have access to the data
/// transfer. If your application should only process this event once per data transfer, the
/// `serial` field can be used to deduplicate it.
DataTransferReceived {
/// ID of the data transfer object, see
/// [`crate::event_loop::ActiveEventLoop::data_transfer`].
id: DataTransferId,
/// Serial returned from `fetch_data_transfer`.
serial: AsyncRequestSerial,
/// The data for the transfer, with a specific type.
value: Arc<dyn TypedData>,
},
/// A drag operation started with `start_drag` has been dropped.
OutgoingDragDropped {
/// The ID returned from `start_drag`
id: DataTransferId,
/// The operation selected by the drop destination.
///
/// This may be `None` if the backend has not supplied a valid action. This is different
/// from the drag being canceled: the drag completed successfully, we just don't know
/// what action was selected.
action: Option<DndAction>,
},
/// A drag operation started with `start_drag` has been canceled.
OutgoingDragCanceled {
/// The ID returned from `start_drag`
id: DataTransferId,
},
/// The window gained or lost focus.
///
@@ -1582,16 +1644,20 @@ mod tests {
use crate::event::Ime::Enabled;
use crate::event::WindowEvent::*;
use crate::event::{PointerKind, PointerSource};
use crate::event_loop::DndAction;
use crate::data_transfer::DataTransferId;
let dnd_data = DataTransferId::from_raw(123);
with_window_event(CloseRequested);
with_window_event(Destroyed);
with_window_event(Focused(true));
with_window_event(Moved((0, 0).into()));
with_window_event(SurfaceResized((0, 0).into()));
with_window_event(DragEntered { paths: vec!["x.txt".into()], position: (0, 0).into() });
with_window_event(DragMoved { position: (0, 0).into() });
with_window_event(DragDropped { paths: vec!["x.txt".into()], position: (0, 0).into() });
with_window_event(DragLeft { position: Some((0, 0).into()) });
with_window_event(DragEntered { id: dnd_data, position: None });
with_window_event(DragPosition { id: dnd_data, position: (0, 0).into(), proposed_action: Some(DndAction::Copy) });
with_window_event(DragDropped { id: dnd_data, proposed_action: Some(DndAction::Copy) });
with_window_event(DragLeft { id: dnd_data });
with_window_event(Ime(Enabled));
with_window_event(PointerMoved {
device_id: None,

View File

@@ -13,9 +13,11 @@ use rwh_06::{DisplayHandle, HandleError, HasDisplayHandle};
use crate::Instant;
use crate::as_any::AsAny;
use crate::cursor::{CustomCursor, CustomCursorSource};
use crate::error::RequestError;
use crate::data_transfer::{DataTransfer, DataTransferId, DataTransferSend, TransferType};
use crate::error::{NotSupportedError, RequestError};
use crate::icon::Icon;
use crate::monitor::MonitorHandle;
use crate::window::{Theme, Window, WindowAttributes};
use crate::window::{Theme, Window, WindowAttributes, WindowId};
pub trait ActiveEventLoop: AsAny + fmt::Debug {
/// Creates an [`EventLoopProxy`] that can be used to dispatch user events
@@ -114,8 +116,136 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug {
/// Get the raw-window-handle handle.
fn rwh_06_handle(&self) -> &dyn HasDisplayHandle;
/// Request to fetch a type from a [data transfer](crate::data_transfer::DataTransfer).
///
/// This may be called multiple times on the same [`DataTransferId`] with different types,
/// and may be called at any point during the drag operation, including during handling the
/// [`DragDropped`](crate::event::WindowEvent::DragDropped) event. After that event has been
/// received, though, the data transfer is not guaranteed to be available. The data is
/// _not_ guaranteed to be available during (or after) handling of
/// [`DragLeft](crate::event::WindowEvent::DragLeft).
///
/// Once available, the data will be supplied to the application with the
/// [`DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) event.
fn fetch_data_transfer(
&self,
id: DataTransferId,
type_: &dyn TransferType,
) -> Result<AsyncRequestSerial, 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<Box<dyn DataTransfer>, RequestError> {
let _ = id;
Err(RequestError::NotSupported(NotSupportedError::new(
DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE,
)))
}
/// Set a given set of `DndAction`s as the valid actions for the given [`DataTransferId`],
/// if the transfer ID is from an incoming drag-and-drop operation.
///
/// This allows the OS/compositor to display the correct UI, indicating that the dragged data
/// can be dropped. If the data transfer does not exist or is not from a drag-and-drop
/// operation, will return an error.
///
/// The operating system will consider the drag either accepted or rejected based on the
/// set of valid actions supplied using this method, combined with the set of valid actions
/// on the drag source. If the drag is rejected at the point that the user finalizes the drop,
/// the application will receive [`DragLeft`](crate::event::WindowEvent::DragLeft) instead
/// of [`DragDropped`](crate::event::WindowEvent::DragDropped).
///
/// Note that _rejecting_ the drag is not the same as _canceling_ the drag. A rejected drag can
/// be accepted later and the user can continue dragging it over other potential targets. On
/// most platforms, there is no way for an application to explicitly cancel a drag
/// operation.
///
/// The set of actions is expected to be ordered by preference.
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), RequestError> {
let _ = id;
let _ = actions;
Err(RequestError::NotSupported(NotSupportedError::new(
DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE,
)))
}
/// Initiate a new drag-and-drop operation.
///
/// See [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSendBuilder) for how to
/// create a new cross-platform data transfer, or [`DataTransferSend`] for a generic trait
/// which can be implemented manually.
///
/// The [`DataTransferId`] returned from this method, identifying the outgoing drag, is
/// currently only used for identifying the drag in the
/// [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped) event. In most
/// cases, a drag will be started while the mouse is over the window which started it. This
/// means that, directly after this method is called, the window will then receive a
/// [`DragEntered`](crate::event::WindowEvent::DragEntered) event. However, the ID identifying
/// the incoming drag is not guaranteed to be the same as the ID returned from this method.
///
/// For most cases, applications can treat all `DragEntered` events the same, whether they were
/// initiated by the same application or a different application. However, if the user wants to
/// have some kind of special handling for internal drag-and-drop, they will currently need
/// to implement it via workaround. On all systems where drag-and-drop is implemented in
/// Winit, the application can make the assumption that only a single drag operation can
/// occur at one time. Therefore, if `DragEntered` is received between calling this method
/// and receiving `OutgoingDragDropped`, then you can assume that it's the same drag.
/// In theory, Wayland allows multiple simultaneous drag operations at a time, but Winit does
/// not currently guarantee that this is supported correctly for either internal or external
/// drag.
///
/// ### Arguments
///
/// - `source` - The ID of the window that initiated the drag operation.
/// - `send_data` - The data provided by this drag operation. See
/// [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSendBuilder).
/// - `actions` - The set of valid actions for this drag operation. See [`DndAction`]. On
/// Wayland, this is expected to be ordered by preference.
/// - `icon` - The icon to show while dragging.
///
/// Some platforms have a more-expressive way of setting the visual component of a drag
/// operation. For those platforms, consider using the platform-specific implementation of
/// [`DataTransferSend`] for `send_data` and set this field to `None`.
///
/// ### Returns
///
/// A unique identifier for this drag operation, which will be later supplied by
/// [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped).
fn start_drag(
&self,
source: WindowId,
send_data: Box<dyn DataTransferSend>,
actions: &[DndAction],
icon: Option<DragIcon>,
) -> Result<DataTransferId, RequestError> {
let _ = source;
let _ = send_data;
let _ = actions;
let _ = icon;
Err(RequestError::NotSupported(NotSupportedError::new(
DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE,
)))
}
}
const DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE: &str = {
"Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on this \
platform"
};
impl HasDisplayHandle for dyn ActiveEventLoop + '_ {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
self.rwh_06_handle().display_handle()
@@ -124,6 +254,73 @@ impl HasDisplayHandle for dyn ActiveEventLoop + '_ {
impl_dyn_casting!(ActiveEventLoop);
/// Information needed to initiate a new drag operation.
pub struct DragIcon {
/// The icon to apply to the cursor.
pub icon: Icon,
/// An x offset applied to the dragged icon.
///
/// This is specified in image pixels. 0 means that the left side of the icon will be at
/// the cursor.
pub offset_x: i32,
/// A y offset applied to the dragged icon.
///
/// This is specified in image pixels. 0 means that the top of the icon will be at the
/// cursor.
pub offset_y: i32,
}
impl From<Icon> for DragIcon {
fn from(value: Icon) -> Self {
Self { icon: value, offset_x: 0, offset_y: 0 }
}
}
/// The set of available actions for a drag operation.
///
/// This is _not_ a bitset, as on some platforms (e.g. Wayland, macOS) the source and/or destination
/// are expected to provide some kind of order of preference.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DndAction {
/// Move the dragged item from the source to the destination.
///
/// # Platforms
///
/// - Wayland
/// - macOS
/// - Windows
Move,
/// Copy the dragged item from the source to the destination.
///
/// # Platforms
///
/// - X11
/// - Wayland
/// - macOS
/// - Windows
Copy,
/// A link is established between the source and the destination.
///
/// # Platforms
///
/// - macOS
/// - Windows
Link,
/// The user will be prompted for what should be done
///
/// # Platforms
///
/// - Wayland
Ask,
/// The source and destination will negotiate the drag operation privately
///
/// # Platforms
///
/// - macOS
Private,
}
/// Control the [`ActiveEventLoop`], possibly from a different thread, without referencing it
/// directly.
#[derive(Clone, Debug)]

View File

@@ -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;

View File

@@ -22,6 +22,7 @@ serde = ["dep:serde", "bitflags/serde", "smol_str/serde", "dpi/serde"]
bitflags.workspace = true
cursor-icon.workspace = true
dpi.workspace = true
percent-encoding.workspace = true
rwh_06.workspace = true
serde = { workspace = true, optional = true }
smol_str.workspace = true
@@ -40,7 +41,7 @@ sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-feature
sctk-adwaita = { version = "0.11.0", default-features = false, optional = true }
wayland-backend = { version = "0.3.10", default-features = false, features = ["client_system"] }
wayland-client = "0.31.10"
wayland-protocols = { version = "0.32.11", features = ["staging", "unstable"] }
wayland-protocols = { version = "0.32.12", features = ["staging", "unstable"] }
wayland-protocols-plasma = { version = "0.3.8", features = ["client"] }
winit-common = { workspace = true, features = ["xkb", "wayland"] }

805
winit-wayland/src/dnd.rs Normal file
View File

@@ -0,0 +1,805 @@
//! Types related to drag-and-drop and data transfer on Wayland.
use std::ffi::OsStr;
use std::fmt;
use std::io::{self, BufRead, Cursor, ErrorKind, Write};
use std::ops::{BitOr, Deref};
use std::sync::Arc;
use calloop::PostAction;
use dpi::{LogicalPosition, PhysicalPosition};
use sctk::data_device_manager::WritePipe;
use sctk::data_device_manager::data_device::{DataDeviceData, DataDeviceHandler};
use sctk::data_device_manager::data_offer::{DataOfferHandler, DragOffer};
use sctk::data_device_manager::data_source::{DataSourceHandler, DragSource as SctkDragSource};
use sctk::reexports::client::backend::ObjectId;
use wayland_client::protocol::wl_data_device::WlDataDevice;
use wayland_client::protocol::wl_data_device_manager::DndAction as WlDndAction;
use wayland_client::protocol::wl_data_offer::WlDataOffer;
use wayland_client::protocol::wl_data_source::WlDataSource;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::{Connection, Proxy, QueueHandle};
use winit_core::data_transfer::{
DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData,
};
use winit_core::event::WindowEvent;
use winit_core::event_loop::DndAction;
use winit_core::window::WindowId;
use crate::make_data_transfer_id;
use crate::state::WinitState;
fn encode_uri_list<I>(uri_list: I) -> Vec<u8>
where
I: IntoIterator,
I::Item: AsRef<OsStr>,
{
let mut out = Vec::new();
for uri in uri_list {
out.extend_from_slice(OsStr::new(&uri).as_encoded_bytes());
out.extend_from_slice(b"\r\n");
}
out
}
impl DataSourceHandler for WinitState {
fn accept_mime(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
_: Option<String>,
) {
// This method isn't a necessary part of the protocol, it's a holdover from the first
// version of DnD in Wayland and now just serves as a hint.
}
fn send_request(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
mime: String,
fd: WritePipe,
) {
let Some(data) = self.dnd_state.send_drag_data_mut() else {
// TODO: Is there a way to explicitly express that the data was not sent?
return;
};
let mime = MimeType::parse(mime);
let Some(send_data) = data.data_for_type(&mime) else {
return;
};
let mut encoder = match send_data {
SendData::Uris(strings) => Cursor::new(encode_uri_list(strings)),
SendData::String(str) => match mime.parse_charset() {
Ok(Charset::Utf8) => Cursor::new(str.into_bytes()),
Err(e) => {
tracing::error!("{e}");
return;
},
},
SendData::Bytes(binary) => Cursor::new(binary),
};
let _ = self.loop_handle.insert_source(fd, move |_, file, _| {
// Safety: We only mutate `file` in-place and do not replace and drop it.
let file = unsafe { file.get_mut() };
loop {
let Ok(encoded_bytes) = encoder.fill_buf() else {
return PostAction::Remove;
};
match file.write(encoded_bytes) {
Ok(0) => {
break PostAction::Remove;
},
Ok(consumed) => {
encoder.consume(consumed);
},
Err(e) if e.kind() == ErrorKind::WouldBlock => {
break PostAction::Continue;
},
Err(_) => {
break PostAction::Remove;
},
}
}
});
}
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
let Some(current_drag) = self.dnd_state.send_drag() else {
return;
};
let window_id = current_drag.window_id;
let id = current_drag.data_transfer_id;
self.events_sink.push_window_event(WindowEvent::OutgoingDragCanceled { id }, window_id);
}
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
let Some(current_drag) = self.dnd_state.send_drag() else {
return;
};
let window_id = current_drag.window_id;
let id = current_drag.data_transfer_id;
let selected_action = current_drag.selected_action;
self.events_sink.push_window_event(
WindowEvent::OutgoingDragDropped {
id,
action: dnd_action_wl_to_winit(selected_action),
},
window_id,
);
}
fn dnd_finished(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &wayland_client::protocol::wl_data_source::WlDataSource,
) {
self.dnd_state.clear_send_drag();
}
fn action(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
_: &WlDataSource,
action: WlDndAction,
) {
self.dnd_state.set_target_drag_action(action);
}
}
#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)]
enum Charset {
#[default]
Utf8,
}
/// MIME type as string, with an optional hint detected from the MIME type.
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct MimeType {
mime: Arc<str>,
hint: Option<TypeHint>,
}
// MIME types
// Files
const TEXT_URI_LIST: &str = "text/uri-list";
// Plaintext
const TEXT_PLAIN: &str = "text/plain";
const TEXT_PLAIN_CHARSET_UTF8: &str = "text/plain; charset=utf-8";
// HTML
const TEXT_HTML: &str = "text/html";
const TEXT_HTML_CHARSET_UTF8: &str = "text/html; charset=utf-8";
// RTF
const APPLICATION_RTF: &str = "application/rtf";
// Audio
const AUDIO_AAC: &str = "audio/aac";
const AUDIO_AIFF: &str = "audio/aiff";
const AUDIO_FLAC: &str = "audio/flac";
const AUDIO_WAV: &str = "audio/wav";
const AUDIO_WAVE: &str = "audio/wave";
const AUDIO_X_WAV: &str = "audio/x-wav";
const AUDIO_VND_WAV: &str = "audio/vnd.wav";
const AUDIO_VND_WAVE: &str = "audio/vnd.wave";
const AUDIO_MPEG: &str = "audio/mpeg";
const AUDIO_OGG: &str = "audio/ogg";
// Image
const IMAGE_BMP: &str = "image/bmp";
const IMAGE_GIF: &str = "image/gif";
const IMAGE_JPEG: &str = "image/jpeg";
const IMAGE_PJPEG: &str = "image/pjpeg";
const IMAGE_PNG: &str = "image/png";
const IMAGE_SVG: &str = "image/svg+xml";
const IMAGE_TIFF: &str = "image/tiff";
const IMAGE_WEBP: &str = "image/webp";
const IMAGE_X_ICON: &str = "image/x-icon";
const IMAGE_RAW: &str = "image/x-panasonic-raw";
#[derive(Debug)]
struct UnexpectedCharsetError<'a>(&'a str);
impl std::error::Error for UnexpectedCharsetError<'_> {}
impl fmt::Display for UnexpectedCharsetError<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unsupported charset: {}", self.0)
}
}
impl MimeType {
const MIME_HINT_MAP: &[(&str, TypeHint)] = &[
// Files
(TEXT_URI_LIST, TypeHint::UriList),
// Plaintext
(TEXT_PLAIN, TypeHint::Plaintext),
(TEXT_PLAIN_CHARSET_UTF8, TypeHint::Plaintext),
// HTML
(TEXT_HTML, TypeHint::Html),
(TEXT_HTML_CHARSET_UTF8, TypeHint::Html),
// RTF
(APPLICATION_RTF, TypeHint::Rtf),
// Audio
(AUDIO_AAC, TypeHint::Audio { extension_hint: Some("aac") }),
(AUDIO_AIFF, TypeHint::Audio { extension_hint: Some("aif") }),
(AUDIO_FLAC, TypeHint::Audio { extension_hint: Some("flac") }),
(AUDIO_VND_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_VND_WAVE, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_WAVE, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_X_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
(AUDIO_OGG, TypeHint::Audio { extension_hint: Some("ogg") }),
(AUDIO_MPEG, TypeHint::Audio { extension_hint: Some("mp3") }),
// Image
(IMAGE_BMP, TypeHint::Image { extension_hint: Some("bmp") }),
(IMAGE_GIF, TypeHint::Image { extension_hint: Some("gif") }),
(IMAGE_JPEG, TypeHint::Image { extension_hint: Some("jpg") }),
(IMAGE_PJPEG, TypeHint::Image { extension_hint: Some("jpg") }),
(IMAGE_PNG, TypeHint::Image { extension_hint: Some("png") }),
(IMAGE_RAW, TypeHint::Image { extension_hint: Some("raw") }),
(IMAGE_SVG, TypeHint::Image { extension_hint: Some("svg") }),
(IMAGE_TIFF, TypeHint::Image { extension_hint: Some("tiff") }),
(IMAGE_WEBP, TypeHint::Image { extension_hint: Some("webp") }),
(IMAGE_X_ICON, TypeHint::Image { extension_hint: Some("ico") }),
];
// Returns an iterator so that things like the multiple charsets for plaintext/HTML
// and the multiple ways of expressing .wav work correctly.
pub(crate) fn from_dyn(type_: &dyn TransferType) -> impl Iterator<Item = Self> {
let downcast = type_.cast_ref::<Self>().cloned();
let downcast_failed = downcast.is_none();
// This filter is a bit hacky, but it's the only way to ensure that we always
// return the same type.
let from_hint = downcast_failed
.then_some(
Self::MIME_HINT_MAP
.iter()
.filter(move |(_, haystack)| TransferType::matches(haystack, type_))
.map(move |(mime, _)| Self {
mime: mime.to_string().into(),
hint: type_.hint(),
}),
)
.into_iter()
.flatten();
downcast.into_iter().chain(from_hint)
}
// TODO: We should properly parse MIME types using `mime` or a similar crate.
fn parse_charset(&self) -> Result<Charset, UnexpectedCharsetError<'_>> {
let Some((_, charset)) = self
.mime
.split_once(';')
.and_then(|(_essence, options)| options.split_once("charset="))
else {
return Ok(Default::default());
};
let charset = charset.split_once(',').map(|(first, _)| first).unwrap_or(charset).trim();
if charset == "utf-8" { Ok(Charset::Utf8) } else { Err(UnexpectedCharsetError(charset)) }
}
fn parse(mime: String) -> Self {
let hint = Self::MIME_HINT_MAP
.iter()
.find_map(|(haystack, hint)| (*haystack == &*mime).then_some(*hint))
.or_else(|| {
if mime.starts_with("image/") {
Some(TypeHint::Image { extension_hint: None })
} else if mime.starts_with("audio/") {
Some(TypeHint::Audio { extension_hint: None })
} else {
None
}
});
Self { mime: mime.into(), hint }
}
}
impl fmt::Display for MimeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.mime.fmt(f)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct UnknownTypeHint(pub TypeHint);
impl fmt::Display for UnknownTypeHint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unknown type hint: {:?}", self.0)
}
}
impl TryFrom<TypeHint> for MimeType {
type Error = UnknownTypeHint;
fn try_from(hint: TypeHint) -> Result<Self, Self::Error> {
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<TypeHint> {
self.hint
}
fn matches(&self, other: &dyn TransferType) -> bool {
if let Some(other_mime) = other.cast_ref::<Self>() {
*self == *other_mime
} else {
// If either hint is `None`, return false
self.hint().is_some_and(|hint| other.hint() == Some(hint))
}
}
}
type BytesResult = Result<Vec<u8>, Arc<io::Error>>;
/// Typed data transfer from another application.
#[derive(Debug)]
pub struct MimeData {
mime_type: MimeType,
result: BytesResult,
}
impl MimeData {
pub(crate) fn new(mime_type: MimeType, result: BytesResult) -> Self {
Self { mime_type, result }
}
fn data(&self) -> io::Result<&[u8]> {
fn arc_to_io_error(arc: Arc<io::Error>) -> io::Error {
io::Error::new(arc.kind(), arc)
}
self.result.as_deref().map_err(|e| arc_to_io_error(e.clone()))
}
}
impl TypedData for MimeData {
fn type_(&self) -> &dyn TransferType {
&self.mime_type
}
fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
let data = self.data().ok()?.to_owned();
Some(Box::new(io::Cursor::new(data)))
}
fn try_as_bytes(&self) -> io::Result<Vec<u8>> {
self.data().map(ToOwned::to_owned)
}
fn try_as_uris(&self) -> io::Result<Vec<String>> {
let data = self.data()?;
Cursor::new(&data)
.lines()
.filter(|result| match result {
Ok(s) => !s.starts_with('#'),
// We want to maintain errors, so the final `collect` returns an error too
Err(_) => true,
})
.collect()
}
fn try_as_string(&self) -> io::Result<String> {
let charset = self.mime_type.parse_charset();
let data = self.data()?;
match charset {
Ok(Charset::Utf8) => String::from_utf8(data.to_vec())
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)),
Err(e) => Err(io::Error::other(e.to_string())),
}
}
}
/// A wrapper around `WlDataOffer`, implementing `DataTransfer`.
#[derive(Debug, Clone)]
pub struct DataOffer {
mime_types: Arc<[MimeType]>,
data: WlDataOffer,
available_actions: WlDndAction,
data_device_id: ObjectId,
serial: u32,
window_id: WindowId,
}
pub(crate) fn dnd_action_winit_to_wl(winit: DndAction) -> WlDndAction {
match winit {
DndAction::Move => WlDndAction::Move,
DndAction::Copy => WlDndAction::Copy,
DndAction::Ask => WlDndAction::Ask,
_ => WlDndAction::empty(),
}
}
pub(crate) fn dnd_action_wl_to_winit(wl: WlDndAction) -> Option<DndAction> {
match wl {
WlDndAction::Move => Some(DndAction::Move),
WlDndAction::Copy => Some(DndAction::Copy),
WlDndAction::Ask => Some(DndAction::Ask),
_ => None,
}
}
impl DataOffer {
pub(crate) fn transfer_id(&self) -> DataTransferId {
make_data_transfer_id(self.data_device_id.clone(), self.serial)
}
pub(crate) fn first_mime_type(&self) -> Option<&MimeType> {
self.mime_types.first()
}
pub(crate) fn serial(&self) -> u32 {
self.serial
}
pub(crate) fn window_id(&self) -> WindowId {
self.window_id
}
pub(crate) fn set_actions(&self, action_set: &[DndAction]) -> bool {
let preferred_action = action_set.iter().find_map(|winit| {
let wl = dnd_action_winit_to_wl(*winit);
self.available_actions.intersects(wl).then_some(wl)
});
let any = preferred_action.is_some();
let all_actions = action_set
.iter()
.copied()
.map(dnd_action_winit_to_wl)
.fold(WlDndAction::empty(), BitOr::bitor);
self.data.set_actions(all_actions, preferred_action.unwrap_or(WlDndAction::empty()));
any
}
pub(crate) fn find_type_dyn<'a>(&'a self, type_: &'a dyn TransferType) -> Option<&'a MimeType> {
match type_.cast_ref::<MimeType>() {
Some(mime_type) => Some(mime_type),
None => {
let hint = type_.hint()?;
self.mime_types.iter().find(|mime_type| {
mime_type.hint().is_some_and(|haystack| haystack.matches(&hint))
})
},
}
}
}
impl Deref for DataOffer {
type Target = WlDataOffer;
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl DataTransfer for DataOffer {
fn for_each_available_type<'this>(
&'this self,
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>,
) {
let _ = self.mime_types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func);
}
}
/// Wrapper for [`WlDataSource`], which exposes the types that are advertised by a data
/// transfer operation, along with the data that the source represents
#[derive(Debug)]
pub struct DragSource {
pub(crate) data_transfer_id: DataTransferId,
/// The `WlDataSource` generated from `data`.
///
/// This is stored internally, as if this source is dropped then the
/// drag operation will be cancelled.
_data_source: SctkDragSource,
/// The supplied [`DataTransferSend`].
pub(crate) data: Box<dyn DataTransferSend>,
pub(crate) selected_action: WlDndAction,
pub(crate) window_id: WindowId,
/// (Optionally) an icon for the drag-and-drop operation.
_icon: Option<WlSurface>,
}
impl DragSource {
pub(crate) fn new(
data_transfer_id: DataTransferId,
data_source: SctkDragSource,
data: Box<dyn DataTransferSend>,
icon: Option<WlSurface>,
window_id: WindowId,
) -> Self {
Self {
data_transfer_id,
_data_source: data_source,
data,
selected_action: WlDndAction::None,
window_id,
_icon: icon,
}
}
/// Per-type data to be sent. See [`DataTransferSend`].
pub fn data(&mut self) -> &mut dyn DataTransferSend {
&mut *self.data
}
}
/// The current state of an in-progress drag-and-drop operation.
#[derive(Debug, Default)]
pub struct DndState {
receive_drag: Option<DataOffer>,
send_drag: Option<DragSource>,
}
impl DndState {
pub(crate) fn receive_drag(&self) -> Option<&DataOffer> {
self.receive_drag.as_ref()
}
pub(crate) fn set_send_drag(&mut self, source: DragSource) {
self.send_drag = Some(source);
}
pub(crate) fn send_drag(&self) -> Option<&DragSource> {
self.send_drag.as_ref()
}
pub(crate) fn set_target_drag_action(&mut self, action: WlDndAction) {
if let Some(source) = &mut self.send_drag {
source.selected_action = action;
}
}
/// Returns `true` if a drag operation was in progress, `false` if no drag operation was in
/// progress.
pub(crate) fn clear_send_drag(&mut self) -> bool {
self.send_drag.take().is_some()
}
pub(crate) fn send_drag_data_mut(&mut self) -> Option<&mut dyn DataTransferSend> {
self.send_drag.as_mut().map(|send| send.data())
}
}
impl DataOfferHandler for WinitState {
fn source_actions(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
offer: &mut DragOffer,
actions: WlDndAction,
) {
let _ = actions;
let _ = offer;
let _ = qh;
let _ = conn;
// Not implemented, but required for `DataDeviceHandler`.
}
fn selected_action(
&mut self,
conn: &Connection,
qh: &QueueHandle<Self>,
offer: &mut DragOffer,
actions: WlDndAction,
) {
let _ = actions;
let _ = offer;
let _ = qh;
let _ = conn;
}
}
impl DataDeviceHandler for WinitState {
fn enter(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
data_device: &WlDataDevice,
x: f64,
y: f64,
wl_surface: &WlSurface,
) {
let Some(data) = data_device.data::<DataDeviceData>() 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::<Vec<_>>()
.into(),
available_actions: drag.source_actions,
serial: drag.serial,
data_device_id: data_device.id(),
data: drag.inner().clone(),
window_id,
});
current_drag.set_actions(&[]);
let id = current_drag.transfer_id();
self.dnd_state.receive_drag = Some(current_drag);
let scale_factor = self
.windows
.borrow()
.get(&window_id)
.map(|window| window.lock().unwrap().scale_factor())
.unwrap_or(1.);
let position: PhysicalPosition<f64> = LogicalPosition::new(x, y).to_physical(scale_factor);
self.events_sink.push_window_event(
WindowEvent::DragEntered { id, position: Some(position) },
window_id,
);
}
fn leave(&mut self, _: &Connection, _: &QueueHandle<Self>, data_device: &WlDataDevice) {
let Some(data) = data_device.data::<DataDeviceData>() else {
return;
};
if let Some(current_drag) = self.dnd_state.receive_drag() {
self.events_sink.push_window_event(
WindowEvent::DragLeft { id: current_drag.transfer_id() },
current_drag.window_id(),
);
if let Some(receive_drag) = self.dnd_state.receive_drag.take() {
receive_drag.finish();
}
}
if let Some(drag) = data.drag_offer() {
drag.destroy();
}
if let Some(selection) = data.selection_offer() {
selection.destroy();
}
}
fn motion(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
data_device: &WlDataDevice,
x: f64,
y: f64,
) {
let Some(data) = data_device.data::<DataDeviceData>() else {
return;
};
let Some(drag) = data.drag_offer() else {
// Selections (copy/paste) are not yet implemented
return;
};
// `selected_action` should only contain a single flag, but we check with `contains`
// just in case we or the compositor misunderstood the spec.
let proposed_action = if drag.selected_action.contains(WlDndAction::Move) {
Some(DndAction::Move)
} else if drag.selected_action.contains(WlDndAction::Copy) {
Some(DndAction::Copy)
} else if drag.selected_action.contains(WlDndAction::Ask) {
Some(DndAction::Ask)
} else {
None
};
let Some(current_drag) = self.dnd_state.receive_drag() else {
return;
};
let window_id = crate::make_wid(&drag.surface);
let scale_factor = self
.windows
.borrow()
.get(&window_id)
.map(|window| window.lock().unwrap().scale_factor())
.unwrap_or(1.);
let position: PhysicalPosition<f64> = LogicalPosition::new(x, y).to_physical(scale_factor);
self.events_sink.push_window_event(
WindowEvent::DragPosition { id: current_drag.transfer_id(), position, proposed_action },
window_id,
);
}
fn selection(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {
// We don't handle selections right now.
}
fn drop_performed(
&mut self,
_: &Connection,
_: &QueueHandle<Self>,
data_device: &WlDataDevice,
) {
let Some(data) = data_device.data::<DataDeviceData>() else {
return;
};
let Some(drag) = data.drag_offer() else {
// Selections (copy/paste) are not yet implemented
return;
};
let Some(current_drag) = self.dnd_state.receive_drag() else {
return;
};
let window_id = crate::make_wid(&drag.surface);
// `selected_action` should only contain a single flag, but we check with `contains`
// just in case we or the compositor misunderstood the spec.
let proposed_action = if drag.selected_action.contains(WlDndAction::Move) {
Some(DndAction::Move)
} else if drag.selected_action.contains(WlDndAction::Copy) {
Some(DndAction::Copy)
} else if drag.selected_action.contains(WlDndAction::Ask) {
Some(DndAction::Ask)
} else {
None
};
self.events_sink.push_window_event(
WindowEvent::DragDropped { id: current_drag.transfer_id(), proposed_action },
window_id,
);
if let Some(receive_drag) = self.dnd_state.receive_drag.take() {
receive_drag.finish();
}
if let Some(drag) = data.drag_offer() {
drag.destroy();
}
if let Some(selection) = data.selection_offer() {
selection.destroy();
}
}
}
sctk::delegate_data_device!(WinitState);

View File

@@ -1,35 +1,46 @@
//! The event-loop routines.
use std::cell::{Cell, RefCell};
use std::io::Result as IOResult;
use std::mem;
use std::io::{self, Read, Result as IOResult};
use std::ops::BitOr;
use std::os::fd::OwnedFd;
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use std::{fmt, mem};
use calloop::PostAction;
use calloop::ping::Ping;
use dpi::LogicalSize;
use rustix::event::{PollFd, PollFlags};
use rustix::pipe::{self, PipeFlags};
use sctk::data_device_manager::{ReadPipe, data_offer};
use sctk::reexports::calloop_wayland_source::WaylandSource;
use sctk::reexports::client::{Connection, QueueHandle, globals};
use sctk::shell::WaylandSurface;
use tracing::warn;
use wayland_client::Proxy;
use wayland_client::protocol::wl_data_device_manager::DndAction as WlDndAction;
use wayland_client::protocol::wl_shm::Format;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::data_transfer::{DataTransfer, DataTransferId, DataTransferSend, TransferType};
use winit_core::error::{EventLoopError, NotSupportedError, OsError, RequestError};
use winit_core::event::{DeviceEvent, StartCause, SurfaceSizeWriter, WindowEvent};
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
OwnedDisplayHandle as CoreOwnedDisplayHandle,
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
DndAction, DragIcon, OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::icon::RgbaIcon;
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
use winit_core::window::Theme;
use crate::dnd::{MimeData, dnd_action_winit_to_wl};
use crate::types::cursor::WaylandCustomCursor;
use crate::{DragSource, MimeType, image_to_buffer, make_data_transfer_id};
mod proxy;
pub mod sink;
@@ -678,8 +689,233 @@ impl RootActiveEventLoop for ActiveEventLoop {
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
fn fetch_data_transfer(
&self,
id: DataTransferId,
type_: &dyn TransferType,
) -> Result<AsyncRequestSerial, RequestError> {
let state = self.state.borrow_mut();
let Some(current_drag) = state.dnd_state.receive_drag() else {
return Err(RequestError::Ignored);
};
if current_drag.transfer_id() != id {
return Err(RequestError::Ignored);
}
let Some(mime_type) = current_drag.find_type_dyn(type_) else {
return Err(RequestError::Ignored);
};
let mime_type_str = mime_type.to_string();
// create a pipe
let (readfd, writefd) =
pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK).map_err(|e| os_error!(e))?;
let async_request_serial = AsyncRequestSerial::get();
let mut buffer = Vec::new();
let window_id = current_drag.window_id();
let mut mime_type = Some(mime_type.clone());
let _ = state.loop_handle.insert_source(ReadPipe::from(readfd), move |_, file, state| {
// SAFETY: We do not overwrite the referent of `file`
let file = unsafe { file.get_mut() };
let result = match file.read_to_end(&mut buffer) {
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
return PostAction::Continue;
},
Ok(0) => Ok(mem::take(&mut buffer)),
Ok(_) => {
return PostAction::Continue;
},
Err(e) => Err(Arc::new(e)),
};
state.events_sink.push_window_event(
WindowEvent::DataTransferReceived {
id,
serial: async_request_serial,
// `unwrap` is safe here, as we always return `PostAction::Remove` in this
// branch.
value: Arc::new(MimeData::new(mime_type.take().unwrap(), result)),
},
window_id,
);
PostAction::Remove
});
current_drag.accept(current_drag.serial(), Some(mime_type_str.clone()));
data_offer::receive_to_fd(current_drag, mime_type_str, writefd);
Ok(async_request_serial)
}
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
let state = self.state.borrow();
let Some(state) = state.dnd_state.receive_drag() else {
return Err(RequestError::Ignored);
};
if state.transfer_id() != id {
return Err(RequestError::Ignored);
}
Ok(Box::new(state.clone()))
}
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), RequestError> {
let state = self.state.borrow();
let Some(state) = state.dnd_state.receive_drag() else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
if state.transfer_id() != id {
return Err(os_error!(UnknownDataTransfer(id)).into());
}
let any_actions = state.set_actions(actions);
let accepted_type =
if any_actions { state.first_mime_type().map(|mime| mime.to_string()) } else { None };
// Some compositors won't even send the "dropped" event if no type
// has been accepted, so we need to accept _something_ here. The
// application can accept further types by fetching the data, but
// this will at least mean that waiting until the drop to start
// fetching data won't prevent the drop from working at all.
state.accept(state.serial(), accepted_type);
Ok(())
}
fn start_drag(
&self,
source: WindowId,
send_data: Box<dyn DataTransferSend>,
action_mask: &[DndAction],
icon: Option<DragIcon>,
) -> Result<DataTransferId, RequestError> {
const NO_POINTER_CAP_ERROR_MSG: &str =
"Tried to initiate drag, but source window does not have the pointer capability";
let mut state = self.state.borrow_mut();
let dnd_actions = action_mask
.iter()
.copied()
.map(dnd_action_winit_to_wl)
.fold(WlDndAction::empty(), BitOr::bitor);
let data_device_manager = state
.data_device_manager_state
.as_ref()
.ok_or(NotSupportedError::new("Tried to initiate drag, but data device not enabled"))?;
let mut mime_types = Vec::new();
send_data.for_each_available_type(&mut |ty_| {
for mime in MimeType::from_dyn(ty_) {
mime_types.push(mime);
}
std::ops::ControlFlow::Continue(())
});
let data_source = data_device_manager.create_drag_and_drop_source(
&self.queue_handle,
mime_types,
dnd_actions,
);
let icon_surface = {
let mut pool = state.image_pool.lock().unwrap();
icon.and_then(|icon| {
let rgba = icon.icon.cast_ref::<RgbaIcon>()?;
let width = rgba.width().try_into().ok()?;
let height = rgba.height().try_into().ok()?;
let buffer =
image_to_buffer(width, height, rgba.buffer(), Format::Argb8888, &mut pool)
.ok()?;
let surface = state.compositor_state.create_surface(&self.queue_handle);
if surface.version() >= 5 {
buffer.attach_to(&surface).ok()?;
surface.offset(icon.offset_x, icon.offset_y);
} else {
surface.attach(Some(buffer.wl_buffer()), icon.offset_x, icon.offset_y);
}
Some(surface)
})
};
// New scope to ensure we drop the locks as soon as possible.
let transfer_id = {
let windows = state.windows.borrow();
let source_window_mutex = windows
.get(&source)
.ok_or(os_error!("Tried to initiate drag, but source window ID was invalid"))?;
let source_window_state = source_window_mutex.lock().unwrap();
let source_surface = source_window_state.window.wl_surface();
let seat = source_window_state
.focused_seats()
.find_map(|seat_id| {
// HACK: How do we get the correct seat for pointers here?
state.seats.get(seat_id).filter(|seat| seat.data_device().is_some())
})
.ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?;
let data_device =
seat.data_device().ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?;
let serial = seat
.pointer_data()
.ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?
.latest_button_serial();
data_source.start_drag(data_device, source_surface, icon_surface.as_ref(), serial);
make_data_transfer_id(data_device.inner().id(), serial)
};
// For some reason, if we commit before starting the drag then the offset isn't applied.
// This doesn't seem to be documented anywhere, and it's possible that it's a bug in KDE.
if let Some(surface) = &icon_surface {
surface.commit();
}
state.dnd_state.set_send_drag(DragSource::new(
transfer_id,
data_source,
send_data,
icon_surface,
source,
));
Ok(transfer_id)
}
}
/// An operation was attempted on a data transfer ID, but that ID was invalid.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct UnknownDataTransfer(pub DataTransferId);
impl fmt::Display for UnknownDataTransfer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.0.into_raw();
write!(f, "Unknown data transfer with ID {id}")
}
}
impl std::error::Error for UnknownDataTransfer {}
impl ActiveEventLoop {
fn clear_exit(&self) {
self.exit.set(None)

View File

@@ -18,13 +18,16 @@
#![allow(clippy::mutable_key_type)]
use std::ffi::c_void;
use std::hash::BuildHasher;
use std::ptr::NonNull;
use dpi::{LogicalSize, PhysicalSize};
use sctk::reexports::client::Proxy;
use sctk::reexports::client::backend::ObjectId;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::shm::slot::{Buffer, CreateBufferError, SlotPool};
use wayland_client::protocol::wl_shm::Format;
use winit_core::data_transfer::DataTransferId;
use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop;
use winit_core::window::{
ActivationToken, PlatformWindowAttributes, Window as CoreWindow, WindowId,
@@ -34,6 +37,7 @@ macro_rules! os_error {
($error:expr) => {{ winit_core::error::OsError::new(line!(), file!(), $error) }};
}
mod dnd;
mod event_loop;
mod output;
mod seat;
@@ -41,6 +45,7 @@ mod state;
mod types;
mod window;
pub use self::dnd::{DataOffer, DragSource, MimeData, MimeType};
pub use self::event_loop::{ActiveEventLoop, EventLoop};
pub use self::window::Window;
@@ -149,6 +154,17 @@ fn make_wid(surface: &WlSurface) -> WindowId {
WindowId::from_raw(surface.id().as_ptr() as usize)
}
/// Create a `DataTransferId` for the given data device and serial.
///
/// It's currently unclear if this will result in the same ID when transferring to the same
/// application.
#[inline]
fn make_data_transfer_id(data_device_id: ObjectId, serial: u32) -> DataTransferId {
const BUILD_HASHER: foldhash::fast::FixedState = foldhash::fast::FixedState::with_seed(0);
DataTransferId::from_raw(BUILD_HASHER.hash_one((data_device_id, serial)) as i64)
}
/// The default routine does floor, but we need round on Wayland.
fn logical_to_physical_rounded(size: LogicalSize<u32>, scale_factor: f64) -> PhysicalSize<u32> {
let width = size.width as f64 * scale_factor;

View File

@@ -3,6 +3,7 @@
use std::sync::Arc;
use foldhash::HashMap;
use sctk::data_device_manager::data_device::DataDevice;
use sctk::reexports::client::backend::ObjectId;
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::protocol::wl_touch::WlTouch;
@@ -64,6 +65,9 @@ pub struct WinitSeatState {
/// The hold pointer gesture bound on the seat.
pointer_gesture_hold: Option<ZwpPointerGestureHoldV1>,
/// The drag-and-drop state
data_device: Option<DataDevice>,
/// The keyboard bound on the seat.
keyboard_state: Option<KeyboardState>,
@@ -78,6 +82,14 @@ impl WinitSeatState {
pub fn new() -> Self {
Default::default()
}
pub(crate) fn data_device(&self) -> Option<&DataDevice> {
self.data_device.as_ref()
}
pub(crate) fn pointer_data(&self) -> Option<&WinitPointerData> {
self.pointer.as_ref().and_then(|pointer| pointer.pointer().data())
}
}
impl SeatHandler for WinitState {
@@ -129,6 +141,11 @@ impl SeatHandler for WinitState {
)
.expect("failed to create pointer with present capability.");
seat_state.data_device = self
.data_device_manager_state
.as_ref()
.map(|device| device.get_data_device(queue_handle, &seat));
seat_state.relative_pointer = self.relative_pointer.as_ref().map(|manager| {
manager.get_relative_pointer(
themed_pointer.pointer(),
@@ -225,6 +242,8 @@ impl SeatHandler for WinitState {
pointer_gesture_hold.destroy();
}
seat_state.data_device = None;
if let Some(pointer) = seat_state.pointer.take() {
let pointer_data = pointer.pointer().winit_data();

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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<ViewporterState>,
/// Data device manager state on the given window.
pub data_device_manager_state: Option<DataDeviceManagerState>,
/// Fractional scaling manager.
pub fractional_scaling_manager: Option<FractionalScalingManager>,
/// Blur manager.
pub blur_manager: Option<BgrEffectManager>,
/// 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(),

View File

@@ -243,6 +243,12 @@ impl WindowState {
}
}
// HACK: Currently to get the data device to initiate a drag-and-drop, we iterate through all
// focused seats to find one with a pointer capability. This is definitely wrong.
pub(crate) fn focused_seats(&self) -> impl Iterator<Item = &ObjectId> {
self.seat_focus.iter()
}
/// Apply closure on the given pointer.
fn apply_on_pointer<F: FnMut(&ThemedPointer<WinitPointerData>, &WinitPointerData)>(
&self,

View File

@@ -19,6 +19,7 @@ rwh_06.workspace = true
serde = { workspace = true, optional = true }
smol_str.workspace = true
tracing.workspace = true
url.workspace = true
winit-core.workspace = true
# Platform-specific
@@ -32,7 +33,9 @@ windows-sys = { workspace = true, features = [
"Win32_Media",
"Win32_System_Com_StructuredStorage",
"Win32_System_Com",
"Win32_System_DataExchange",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Ole",
"Win32_Security",
"Win32_System_SystemInformation",

View File

@@ -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,

1615
winit-win32/src/dnd.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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<dyn Fn(WindowEvent)>,
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<dyn Fn(WindowEvent)>) -> 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<F>(data_obj: *const IDataObject, mut callback: F) -> Option<HDROP>
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::<IDataObjectVtbl>()).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,
};

View File

@@ -63,6 +63,9 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
};
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource};
use winit_core::data_transfer::{
DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData,
};
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
use winit_core::event::{
DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton,
@@ -70,8 +73,8 @@ use winit_core::event::{
};
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider,
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
DndAction, DragIcon, EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider,
OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::keyboard::ModifiersState;
@@ -82,8 +85,9 @@ pub(super) use self::runner::{Event, EventLoopRunner};
use super::SelectedCursor;
use super::window::set_skip_taskbar;
use crate::dark_mode::try_theme;
use crate::dnd::{DropSource, FileDropHandler, SourceDataObject, WinDataTransfer, WinTypedData};
use crate::dpi::{become_dpi_aware, dpi_to_scale_factor};
use crate::drop_handler::FileDropHandler;
use crate::event_loop::runner::PendingDrag;
use crate::icon::WinCursor;
use crate::ime::ImeContext;
use crate::keyboard::KeyEventBuilder;
@@ -478,6 +482,105 @@ impl RootActiveEventLoop for ActiveEventLoop {
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
fn fetch_data_transfer(
&self,
id: DataTransferId,
type_: &dyn TransferType,
) -> Result<AsyncRequestSerial, RequestError> {
let Some(state) = self.0.drag_state(id) else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
let hint = type_.hint().ok_or(RequestError::Ignored)?;
let typed_data = WinTypedData::new(state.data.clone(), hint)
.map(|value| Arc::new(value) as Arc<dyn TypedData>)
.ok_or(RequestError::Ignored)?;
let serial = AsyncRequestSerial::get();
self.0.send_event(Event::Window {
window_id: state.window_id,
event: WindowEvent::DataTransferReceived { id, serial, value: typed_data },
});
Ok(serial)
}
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
let Some(state) = self.0.drag_state(id) else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
Ok(Box::new(WinDataTransfer::new(state.data.clone())))
}
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), RequestError> {
let mut state = self.0.drag_state.borrow_mut();
let Some(state) = state.as_mut().filter(|s| s.id == id) else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
state.actions = actions.to_vec();
Ok(())
}
fn start_drag(
&self,
source: WindowId,
send_data: Box<dyn DataTransferSend>,
allowed_actions: &[DndAction],
icon: Option<DragIcon>,
) -> Result<DataTransferId, RequestError> {
let allowed_effects = crate::dnd::dnd_actions_to_dropeffect_mask(allowed_actions);
// Win32 would happily run a modal `DoDragDrop` with `allowed_effects == 0`, but every
// target would see "no action allowed" and the drag would end in a guaranteed cancel
// after burning a full modal pump. Fail fast instead - the caller asked for a drag
// they explicitly refuse to allow.
if allowed_effects == 0 {
return Err(
NotSupportedError::new("start_drag called with an empty action mask").into()
);
}
let id = crate::dnd::next_data_transfer_id();
let data_object = SourceDataObject::new(send_data);
let drop_source = DropSource::new();
// Attach a drag preview if the app supplied one. Cosmetic failures must not abort the
// drag - the gesture still works, just without a custom image - so log and move on.
if let Some(icon) = icon {
if let Some(rgba) = icon.icon.cast_ref::<winit_core::icon::RgbaIcon>() {
let result = unsafe {
crate::dnd::apply_drag_image(
data_object.interface_ptr() as *mut _,
rgba.width(),
rgba.height(),
rgba.buffer(),
icon.offset_x,
icon.offset_y,
)
};
if let Err(hr) = result {
tracing::warn!("Failed to attach drag image: hr=0x{hr:08x}");
}
} else {
tracing::warn!("DragIcon::icon must be an RgbaIcon on win32; ignoring");
}
}
self.0.pending_drag.replace(Some(PendingDrag {
window_id: source,
id,
data_object,
drop_source,
allowed_effects,
}));
Ok(id)
}
}
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
@@ -487,6 +590,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop {
}
}
/// An operation was attempted on a data transfer ID, but that ID was invalid.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct UnknownDataTransfer(pub DataTransferId);
impl fmt::Display for UnknownDataTransfer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.0.into_raw();
write!(f, "Unknown data transfer with ID {id}")
}
}
impl std::error::Error for UnknownDataTransfer {}
#[derive(Clone)]
pub(crate) struct OwnedDisplayHandle;
@@ -1177,6 +1293,14 @@ unsafe fn public_window_callback_inner(
result = ProcResult::Value(0);
},
WM_PAINT if userdata.event_loop_runner.source_drag.get().is_some() => {
// While a source-side drag is in flight, the app handler is on the stack (we're
// inside `start_drag` -> `DoDragDrop`), so we can neither dispatch `RedrawRequested`
// nor keep re-arming via `RDW_INTERNALPAINT` (that would spin in OLE's modal loop).
// Let `DefWindowProcW` validate the region and show stale content for the duration of
// the drag; the next real paint happens once `DoDragDrop` returns.
result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) });
},
WM_PAINT => {
userdata.window_state_lock().redraw_requested =
userdata.event_loop_runner.should_buffer();
@@ -2352,6 +2476,13 @@ unsafe fn public_window_callback_inner(
.catch_unwind(callback)
.unwrap_or_else(|| result = ProcResult::Value(-1));
// We execute a new drag operation here instead of immediately starting it in
// `ActiveEventLoop::start_drag`. `DoDragDrop` is blocking and synchronous, so if we started
// it inside the event loop then an internal drag operation would be re-entrant and the
// application would not be able to handle the incoming messages. This is after the application
// has had a chance to handle mouse events.
userdata.event_loop_runner.try_execute_drag_drop();
match result {
ProcResult::DefWindowProc(wparam) => unsafe { DefWindowProcW(window, msg, wparam, lparam) },
ProcResult::Value(val) => val,

View File

@@ -1,5 +1,5 @@
use std::any::Any;
use std::cell::{Cell, RefCell};
use std::cell::{Cell, Ref, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
@@ -7,18 +7,49 @@ use std::time::Instant;
use std::{fmt, mem, panic};
use dpi::PhysicalSize;
use windows_sys::Win32::Foundation::HWND;
use windows_sys::Win32::Foundation::{DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, HWND};
use windows_sys::Win32::System::Ole::{
DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, DoDragDrop,
};
use winit_core::application::ApplicationHandler;
use winit_core::data_transfer::DataTransferId;
use winit_core::event::{DeviceEvent, DeviceId, StartCause, SurfaceSizeWriter, WindowEvent};
use winit_core::event_loop::ActiveEventLoop as RootActiveEventLoop;
use winit_core::event_loop::{ActiveEventLoop as RootActiveEventLoop, DndAction};
use winit_core::window::WindowId;
use super::{ActiveEventLoop, ControlFlow, EventLoopThreadExecutor};
use crate::dnd::{DataObject, DropEffect, DropSource, SourceDataObject, drop_effect_to_dnd_action};
use crate::event_loop::{GWL_USERDATA, WindowData};
use crate::util::get_window_long;
type EventHandler = Cell<Option<&'static mut (dyn ApplicationHandler + 'static)>>;
/// State for the single drag-and-drop transfer currently in flight (OLE guarantees at most one
/// active drag per process).
#[derive(Debug)]
pub(super) struct DragState {
pub(super) id: DataTransferId,
pub(super) window_id: WindowId,
pub(super) data: Arc<DataObject>,
pub(super) actions: Vec<DndAction>,
}
pub(super) struct PendingDrag {
pub(super) window_id: WindowId,
pub(super) data_object: SourceDataObject,
pub(super) drop_source: DropSource,
pub(super) allowed_effects: DropEffect,
pub(super) id: DataTransferId,
}
/// Set while `DoDragDrop` is on the call stack - i.e., this process is the source of an active
/// drag. The target-side `IDropTarget` checks this to recognize self-drops and reuse the source's
/// id + allowed actions instead of waiting for the (buffered) app `DragEntered` handler.
#[derive(Copy, Clone)]
pub(crate) struct SourceDrag {
pub(crate) id: DataTransferId,
}
pub(crate) struct EventLoopRunner {
pub(super) thread_id: u32,
@@ -37,6 +68,29 @@ pub(crate) struct EventLoopRunner {
event_handler: Rc<EventHandler>,
event_buffer: RefCell<VecDeque<Event>>,
/// The currently in-flight drag transfer, if any, alive between `DragEntered` and
/// `DragLeft`/`DragDropped`.
pub(super) drag_state: RefCell<Option<DragState>>,
/// `Some(_)` while `start_drag` has `DoDragDrop` on the call stack.
pub(crate) source_drag: Cell<Option<SourceDrag>>,
/// `DoDragDrop` is blocking and synchronous, so we wait until after the application returns
/// control to winit before actually calling into the OS to initiate the drag. This prevents
/// the event loop from being re-entrant if we are doing an internal drag operation, since if
/// we handled this inside `ActiveEventLoop::start_drag` then all the `WindowEvent::Drag*`
/// events would be buffered until `DoDragDrop` returns, preventing the application from
/// handling those messages.
pub(super) pending_drag: RefCell<Option<PendingDrag>>,
/// 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<Option<DataTransferId>>,
panic_error: Cell<Option<PanicError>>,
}
@@ -87,9 +141,122 @@ impl EventLoopRunner {
last_events_cleared: Cell::new(Instant::now()),
event_handler: Rc::new(Cell::new(None)),
event_buffer: RefCell::new(VecDeque::new()),
drag_state: RefCell::new(None),
source_drag: Cell::new(None),
pending_drag: RefCell::new(None),
pending_source_drag_cleanup: Cell::new(None),
}
}
pub(super) fn try_execute_drag_drop(self: &Rc<Self>) {
let Some(PendingDrag { data_object, drop_source, id, allowed_effects, window_id }) =
self.pending_drag.take()
else {
return;
};
// Make the drag visible to our own target-side `IDropTarget` so it can recognize
// self-drops and reuse this id + action mask without going through the (buffered) app
// handler. The guard ensures the flag is cleared on any exit path - if anything between
// here and `DoDragDrop`'s return panics, the stale flag would otherwise permanently
// disable `WM_PAINT` dispatch and misclassify all future external drags as self-drops.
struct ClearOnDrop<'a>(&'a Cell<Option<SourceDrag>>);
impl Drop for ClearOnDrop<'_> {
fn drop(&mut self) {
self.0.set(None);
}
}
self.source_drag.set(Some(SourceDrag { id }));
let _guard = ClearOnDrop(&self.source_drag);
let mut effect_out: u32 = DROPEFFECT_NONE;
let hr = unsafe {
DoDragDrop(
data_object.interface_ptr(),
drop_source.interface_ptr(),
allowed_effects,
&mut effect_out,
)
};
if hr == DRAGDROP_S_DROP {
let action = drop_effect_to_dnd_action(effect_out);
self.send_event(Event::Window {
window_id,
event: WindowEvent::OutgoingDragDropped { id, action },
});
} else if hr == DRAGDROP_S_CANCEL {
self.send_event(Event::Window {
window_id,
event: WindowEvent::OutgoingDragCanceled { id },
});
} else {
tracing::error!("DoDragDrop failed: 0x{hr:08x}");
return;
}
// Both `DRAGDROP_S_DROP` and `DRAGDROP_S_CANCEL` are success codes for us - the app
// will hear about the outcome via the buffered `DragDropped`/`DragLeft` events
// (target-side translates `effect_out == DROPEFFECT_NONE` to `DragLeft`).
// Log the negotiated effect so cross-process drops, which have no target-side event
// in this process, leave a debuggable trace of what action the remote target performed.
tracing::trace!(
"DoDragDrop completed: hr=0x{hr:08x} effect_out={effect_out} (COPY={DROPEFFECT_COPY}, \
MOVE={DROPEFFECT_MOVE}, LINK={DROPEFFECT_LINK})",
);
}
pub(crate) fn defer_source_drag_cleanup(&self, id: DataTransferId) {
self.pending_source_drag_cleanup.set(Some(id));
}
pub(crate) fn register_data_transfer(
&self,
id: DataTransferId,
window_id: WindowId,
data: Arc<DataObject>,
) {
// By default, no actions have been set as valid by the target.
*self.drag_state.borrow_mut() =
Some(DragState { id, window_id, data, actions: Default::default() });
}
pub(crate) fn remove_data_transfer(&self, id: DataTransferId) {
let mut state = self.drag_state.borrow_mut();
if state.as_ref().is_some_and(|s| s.id == id) {
*state = None;
}
}
pub(super) fn drag_state(&self, id: DataTransferId) -> Option<Ref<'_, DragState>> {
Ref::filter_map(self.drag_state.borrow(), |state| state.as_ref().filter(|s| s.id == id))
.ok()
}
pub(crate) fn current_drag_actions(&self, id: DataTransferId) -> Ref<'_, [DndAction]> {
Ref::map(self.drag_state.borrow(), |state| {
state.as_ref().filter(|s| s.id == id).map(|s| &s.actions[..]).unwrap_or_default()
})
}
pub(crate) fn proposed_dnd_action(
&self,
id: DataTransferId,
effects: DropEffect,
) -> Option<DndAction> {
self.current_drag_actions(id).iter().copied().find(|action| {
let effect = match action {
DndAction::Move => DROPEFFECT_MOVE,
DndAction::Copy => DROPEFFECT_COPY,
DndAction::Link => DROPEFFECT_LINK,
_ => return false,
};
(effects & effect) != 0
})
}
/// Associate the application's event handler with the runner.
///
/// # Safety
@@ -138,12 +305,20 @@ impl EventLoopRunner {
last_events_cleared: _,
event_handler,
event_buffer: _,
drag_state,
source_drag,
pending_drag,
pending_source_drag_cleanup,
} = self;
interrupt_msg_dispatch.set(false);
runner_state.set(RunnerState::Uninitialized);
panic_error.set(None);
exit.set(None);
event_handler.set(None);
drag_state.take();
source_drag.set(None);
pending_drag.take();
pending_source_drag_cleanup.set(None);
}
}
@@ -285,6 +460,11 @@ impl EventLoopRunner {
None => break,
}
}
// The app's buffered `DragDropped` handler (if any) has now had its chance to call
// `data_transfer(id)`; safe to release the cached `DragState` for a deferred self-drop.
if let Some(id) = self.pending_source_drag_cleanup.take() {
self.remove_data_transfer(id);
}
}
/// Dispatch control flow events (`NewEvents`, `AboutToWait`, and

View File

@@ -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;

View File

@@ -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)

View File

@@ -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<Theme>,

View File

@@ -13,7 +13,7 @@ macro_rules! atom_manager {
/// Indices into the `Atoms` struct.
#[derive(Copy, Clone, Debug)]
#[allow(non_camel_case_types)]
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
pub enum AtomName {
$($name,)*
}
@@ -34,6 +34,7 @@ macro_rules! atom_manager {
atom_manager! {
// General Use Atoms
CARD32,
STRING,
UTF8_STRING,
WM_CHANGE_STATE,
WM_CLIENT_MACHINE,
@@ -87,11 +88,39 @@ atom_manager! {
XdndDrop,
XdndPosition,
XdndStatus,
XdndActionPrivate,
XdndActionCopy,
XdndSelection,
XdndFinished,
XdndTypeList,
// MIME types for reading selections
TextUriList: b"text/uri-list",
TextPlain: b"text/plain",
TextPlainCharsetUtf8: b"text/plain;charset=utf-8",
TextHtml: b"text/html",
TextHtmlCharsetUtf8: b"text/html;charset=utf-8",
ApplicationRtf: b"application/rtf",
AudioAac: b"audio/aac",
AudioAiff: b"audio/aiff",
AudioFlac: b"audio/flac",
AudioWav: b"audio/wav",
AudioWave: b"audio/wave",
AudioXWav: b"audio/x-wav",
AudioVndWav: b"audio/vnd.wav",
AudioVndWave: b"audio/vnd.wave",
AudioMpeg: b"audio/mpeg",
AudioOgg: b"audio/ogg",
ImageBmp: b"image/bmp",
ImageGif: b"image/gif",
ImageJpeg: b"image/jpeg",
ImagePjpeg: b"image/pjpeg",
ImagePng: b"image/png",
ImageSvg: b"image/svg+xml",
ImageTiff: b"image/tiff",
ImageWebp: b"image/webp",
ImageXIcon: b"image/x-icon",
ImageRaw: b"image/x-panasonic-raw",
None: b"None",
// Miscellaneous Atoms

View File

@@ -1,18 +1,19 @@
use std::collections::VecDeque;
use std::io;
use std::os::raw::*;
use std::path::{Path, PathBuf};
use std::str::Utf8Error;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use dpi::PhysicalPosition;
use percent_encoding::percent_decode;
use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData};
use winit_core::event_loop::AsyncRequestSerial;
use x11rb::protocol::xproto::{self, ConnectionExt};
use crate::atoms::AtomName::None as DndNone;
use crate::atoms::*;
use crate::event_loop::{CookieResultExt, X11Error};
use crate::util;
use crate::xdisplay::XConnection;
use crate::{XWindow, util};
#[derive(Debug, Clone, Copy)]
pub enum DndState {
@@ -21,98 +22,264 @@ pub enum DndState {
}
#[derive(Debug)]
pub enum DndDataParseError {
pub enum UriListParseError {
EmptyData,
InvalidUtf8(#[allow(dead_code)] Utf8Error),
HostnameSpecified(#[allow(dead_code)] String),
UnexpectedProtocol(#[allow(dead_code)] String),
UnresolvablePath(#[allow(dead_code)] io::Error),
Io(#[allow(dead_code)] io::Error),
}
impl From<Utf8Error> for DndDataParseError {
impl From<Utf8Error> for UriListParseError {
fn from(e: Utf8Error) -> Self {
DndDataParseError::InvalidUtf8(e)
UriListParseError::InvalidUtf8(e)
}
}
impl From<io::Error> for DndDataParseError {
impl From<io::Error> for UriListParseError {
fn from(e: io::Error) -> Self {
DndDataParseError::UnresolvablePath(e)
UriListParseError::UnresolvablePath(e)
}
}
#[derive(Debug)]
pub struct SelectionReader {
type_: SelectionType,
data: Vec<u8>,
}
impl TypedData for SelectionReader {
fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
Some(Box::new(io::Cursor::new(self.data.clone())))
}
fn type_(&self) -> &dyn TransferType {
&self.type_
}
fn try_as_string(&self) -> io::Result<String> {
fn invalid_data<E>(err: E) -> io::Error
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
io::Error::new(io::ErrorKind::InvalidData, err)
}
fn decode_utf16_bytes(bytes: &[u8]) -> io::Result<String> {
let utf16 = bytes
.chunks_exact(2)
.map(|chunk| {
let bytes: &[u8; 2] = chunk.try_into().unwrap();
u16::from_ne_bytes(*bytes)
})
.collect::<Vec<_>>();
String::from_utf16(&utf16).map_err(invalid_data)
}
match self.type_.hint() {
Some(TypeHint::Plaintext) | Some(TypeHint::Html) => std::str::from_utf8(&self.data)
.map(|str| str.to_owned())
.map_err(invalid_data)
.or_else(|_| decode_utf16_bytes(&self.data)),
Some(TypeHint::UriList) => String::from_utf8(self.data.clone()).map_err(invalid_data),
_ => Err(io::ErrorKind::InvalidData.into()),
}
}
fn try_as_uris(&self) -> io::Result<Vec<String>> {
if self.type_().hint() != Some(TypeHint::UriList) {
return Err(io::ErrorKind::InvalidData.into());
}
Ok(self
.try_as_string()?
.split(['\n', '\r'])
.filter(|s| !s.is_empty())
.map(ToOwned::to_owned)
.collect())
}
}
#[derive(Debug)]
pub struct DragState {
// Populated by XdndEnter event handler
pub version: c_long,
pub transfer_id: DataTransferId,
pub types: Arc<[SelectionType]>,
// Populated by Xdnd* event handlers
pub source_window: xproto::Window,
// Populated by Xdnd* event handlers
pub target_window: xproto::Window,
// Populated by `fetch_data_transfer`
pub pending_fetch_types: VecDeque<(AsyncRequestSerial, SelectionType)>,
pub finished: Option<(XWindow, XWindow)>,
/// Whether the drag operation is accepted (or `None` if the user never indicated that it's
/// accepted or rejected)
// Populated by `Window::accept_drag`/`Window::reject_drag`.
pub accepted: bool,
}
impl Default for DragState {
fn default() -> Self {
static DATA_TRANSFER_ID: AtomicI64 = AtomicI64::new(0);
Self {
version: Default::default(),
transfer_id: DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)),
types: Default::default(),
source_window: Default::default(),
target_window: Default::default(),
pending_fetch_types: Default::default(),
finished: None,
accepted: Default::default(),
}
}
}
#[derive(Debug)]
pub struct Dnd {
xconn: Arc<XConnection>,
// Populated by XdndEnter event handler
pub version: Option<c_long>,
pub type_list: Option<Vec<xproto::Atom>>,
// Populated by XdndPosition event handler
pub source_window: Option<xproto::Window>,
// Populated by XdndPosition event handler
pub position: PhysicalPosition<f64>,
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
pub result: Option<Result<Vec<PathBuf>, DndDataParseError>>,
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
pub dragging: bool,
// If `None`, no drag operation is in progress.
state: Option<DragState>,
}
#[derive(Debug)]
pub struct Selection {
types: Arc<[SelectionType]>,
}
impl Selection {
pub(crate) fn new(types: Arc<[SelectionType]>) -> Selection {
Selection { types }
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SelectionType {
hint: Option<TypeHint>,
atom: xproto::Atom,
}
impl SelectionType {
pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self {
let atom_to_hint = [
// Files
(atoms[TextUriList], TypeHint::UriList),
// Plaintext
(atoms[STRING], TypeHint::Plaintext),
(atoms[UTF8_STRING], TypeHint::Plaintext),
(atoms[TextPlain], TypeHint::Plaintext),
(atoms[TextPlainCharsetUtf8], TypeHint::Plaintext),
// HTML
(atoms[TextHtml], TypeHint::Html),
(atoms[TextHtmlCharsetUtf8], TypeHint::Html),
// RTF
(atoms[ApplicationRtf], TypeHint::Rtf),
// Audio
(atoms[AudioAac], TypeHint::Audio { extension_hint: Some("aac") }),
(atoms[AudioAiff], TypeHint::Audio { extension_hint: Some("aif") }),
(atoms[AudioFlac], TypeHint::Audio { extension_hint: Some("flac") }),
(atoms[AudioVndWav], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioVndWave], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioWav], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioWave], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioXWav], TypeHint::Audio { extension_hint: Some("wav") }),
(atoms[AudioOgg], TypeHint::Audio { extension_hint: Some("ogg") }),
(atoms[AudioMpeg], TypeHint::Audio { extension_hint: Some("mp3") }),
// Image
(atoms[ImageBmp], TypeHint::Image { extension_hint: Some("bmp") }),
(atoms[ImageGif], TypeHint::Image { extension_hint: Some("gif") }),
(atoms[ImageJpeg], TypeHint::Image { extension_hint: Some("jpg") }),
(atoms[ImagePjpeg], TypeHint::Image { extension_hint: Some("jpg") }),
(atoms[ImagePng], TypeHint::Image { extension_hint: Some("png") }),
(atoms[ImageRaw], TypeHint::Image { extension_hint: Some("raw") }),
(atoms[ImageSvg], TypeHint::Image { extension_hint: Some("svg") }),
(atoms[ImageTiff], TypeHint::Image { extension_hint: Some("tiff") }),
(atoms[ImageWebp], TypeHint::Image { extension_hint: Some("webp") }),
(atoms[ImageXIcon], TypeHint::Image { extension_hint: Some("ico") }),
];
let hint =
atom_to_hint.iter().find_map(|(haystack, hint)| (*haystack == atom).then_some(*hint));
Self { hint, atom }
}
pub fn atom(&self) -> xproto::Atom {
self.atom
}
}
impl TransferType for SelectionType {
fn hint(&self) -> Option<TypeHint> {
self.hint
}
fn matches(&self, other: &dyn TransferType) -> bool {
if let Some(other_mime) = other.cast_ref::<Self>() {
*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<XConnection>) -> Result<Self, X11Error> {
Ok(Dnd {
xconn,
version: None,
type_list: None,
source_window: None,
position: PhysicalPosition::default(),
result: None,
dragging: false,
})
pub fn new(xconn: Arc<XConnection>) -> Self {
Dnd { xconn, state: None }
}
pub fn reset(&mut self) {
self.version = None;
self.type_list = None;
self.source_window = None;
self.result = None;
self.dragging = false;
pub fn state(&self) -> Option<&DragState> {
self.state.as_ref()
}
pub unsafe fn send_status(
&self,
this_window: xproto::Window,
pub fn state_mut(&mut self) -> Option<&mut DragState> {
self.state.as_mut()
}
pub fn find_type_by_hint(&self, hint: TypeHint) -> Option<&SelectionType> {
self.state.as_ref()?.types.iter().find(|haystack| haystack.hint() == Some(hint))
}
pub fn init_state(
&mut self,
version: c_long,
source_window: xproto::Window,
target_window: xproto::Window,
state: DndState,
) -> Result<(), X11Error> {
let atoms = self.xconn.atoms();
let (accepted, action) = match state {
DndState::Accepted => (1, atoms[XdndActionPrivate]),
DndState::Rejected => (0, atoms[DndNone]),
};
self.xconn
.send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [
this_window,
accepted,
0,
0,
action as _,
])?
.ignore_error();
Ok(())
types: Arc<[SelectionType]>,
) -> &DragState {
self.state.get_or_insert(DragState {
version,
types,
source_window,
target_window,
..Default::default()
})
}
pub unsafe fn send_finished(
&self,
this_window: xproto::Window,
target_window: xproto::Window,
state: DndState,
) -> Result<(), X11Error> {
let atoms = self.xconn.atoms();
let (accepted, action) = match state {
DndState::Accepted => (1, atoms[XdndActionPrivate]),
DndState::Rejected => (0, atoms[DndNone]),
let Some(state) = &self.state else {
return Err(X11Error::UnexpectedNull(
"Drag-and-drop state was not initialized (called `send_finished` before XdndEnter",
));
};
let (accepted, action) =
if state.accepted { (1, atoms[XdndActionCopy]) } else { (0, atoms[DndNone]) };
self.xconn
.send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [
this_window,
@@ -138,54 +305,54 @@ impl Dnd {
)
}
pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) {
pub fn convert_selection(
&self,
window: xproto::Window,
time: xproto::Timestamp,
new_type: xproto::Atom,
) {
let atoms = self.xconn.atoms();
self.xconn
.xcb_connection()
.convert_selection(
window,
atoms[XdndSelection],
atoms[TextUriList],
atoms[XdndSelection],
time,
)
// TODO: We store the converted selection back to `XdndSelection`. We should store to
// some new place so that `XdndSelection` remains untouched.
.convert_selection(window, atoms[XdndSelection], new_type, atoms[XdndSelection], time)
.expect_then_ignore_error("Failed to send XdndSelection event")
}
pub unsafe fn read_data(
pub unsafe fn send_status(
&self,
this_window: xproto::Window,
target_window: xproto::Window,
status: DndState,
) -> Result<(), X11Error> {
let atoms = self.xconn.atoms();
let (accepted, action) = match status {
DndState::Accepted => (1, atoms[XdndActionCopy]),
DndState::Rejected => (0, atoms[DndNone]),
};
self.xconn
.send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [
this_window,
accepted,
0,
0,
action as _,
])?
.ignore_error();
Ok(())
}
pub fn read_data(
&self,
window: xproto::Window,
) -> Result<Vec<c_uchar>, util::GetPropertyError> {
type_: SelectionType,
) -> Result<SelectionReader, util::GetPropertyError> {
let atoms = self.xconn.atoms();
self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList])
}
let type_atom = type_.atom();
let bytes = self.xconn.get_property(window, atoms[XdndSelection], type_atom)?;
pub fn parse_data(&self, data: &mut [c_uchar]) -> Result<Vec<PathBuf>, DndDataParseError> {
if !data.is_empty() {
let mut path_list = Vec::new();
let decoded = percent_decode(data).decode_utf8()?.into_owned();
for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) {
// The format is specified as protocol://host/path
// However, it's typically simply protocol:///path
let path_str = if uri.starts_with("file://") {
let path_str = uri.replace("file://", "");
if !path_str.starts_with('/') {
// A hostname is specified
// Supporting this case is beyond the scope of my mental health
return Err(DndDataParseError::HostnameSpecified(path_str));
}
path_str
} else {
// Only the file protocol is supported
return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned()));
};
let path = Path::new(&path_str).canonicalize()?;
path_list.push(path);
}
Ok(path_list)
} else {
Err(DndDataParseError::EmptyData)
}
Ok(SelectionReader { type_, data: bytes })
}
}

View File

@@ -19,12 +19,13 @@ use tracing::warn;
use winit_common::xkb::Context;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, RequestError};
use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType};
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
use winit_core::event::{DeviceId, StartCause, WindowEvent};
use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider,
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
DndAction, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider,
OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
@@ -36,14 +37,17 @@ use x11rb::protocol::{xkb, xproto};
use x11rb::x11_utils::X11Error as LogicalError;
use x11rb::xcb_ffi::ReplyOrIdError;
use crate::atoms::*;
use crate::atoms::{
_NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms,
WM_DELETE_WINDOW,
};
use crate::dnd::Dnd;
use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN};
use crate::ime::{self, Ime, ImeCreationError, ImeSender};
use crate::util::{self, CustomCursor};
use crate::window::{UnownedWindow, Window};
use crate::xdisplay::{XConnection, XError, XNotSupported};
use crate::{XlibErrorHook, ffi, xsettings};
use crate::{Selection, SelectionType, XlibErrorHook, ffi, xsettings};
// Xinput constants not defined in x11rb
pub(crate) const ALL_DEVICES: u16 = 0;
@@ -168,6 +172,7 @@ impl<T> PeekableReceiver<T> {
#[derive(Debug)]
pub struct ActiveEventLoop {
pub(crate) xconn: Arc<XConnection>,
pub(crate) dnd: RefCell<Dnd>,
pub(crate) wm_delete_window: xproto::Atom,
pub(crate) net_wm_ping: xproto::Atom,
pub(crate) net_wm_sync_request: xproto::Atom,
@@ -226,8 +231,7 @@ impl EventLoop {
let net_wm_ping = atoms[_NET_WM_PING];
let net_wm_sync_request = atoms[_NET_WM_SYNC_REQUEST];
let dnd = Dnd::new(Arc::clone(&xconn))
.expect("Failed to call XInternAtoms when initializing drag and drop");
let dnd = Dnd::new(Arc::clone(&xconn)).into();
let (ime_sender, ime_receiver) = mpsc::channel();
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
@@ -342,6 +346,7 @@ impl EventLoop {
let window_target = ActiveEventLoop {
ime,
dnd,
root,
control_flow: Cell::new(ControlFlow::default()),
exit: Cell::new(None),
@@ -368,7 +373,6 @@ impl EventLoop {
let event_processor = EventProcessor {
target: window_target,
dnd,
devices: Default::default(),
randr_event_offset,
ime_receiver,
@@ -621,9 +625,8 @@ impl EventLoop {
fn drain_events<A: ApplicationHandler>(&mut self, app: &mut A) {
let mut xev = MaybeUninit::uninit();
while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } {
let mut xev = unsafe { xev.assume_init() };
self.event_processor.process_event(&mut xev, app);
while let Some(xev) = self.event_processor.poll_one_event(&mut xev) {
self.event_processor.process_event(xev, app);
}
}
@@ -759,6 +762,90 @@ impl RootActiveEventLoop for ActiveEventLoop {
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
let dnd = self.dnd.borrow();
if dnd.state().is_none_or(|state| state.transfer_id != id) {
return Err(RequestError::Ignored);
}
let Some(state) = dnd.state() else {
return Err(RequestError::Ignored);
};
Ok(Box::new(Selection::new(state.types.clone())))
}
fn fetch_data_transfer(
&self,
id: DataTransferId,
type_: &dyn TransferType,
) -> Result<AsyncRequestSerial, RequestError> {
let mut dnd = self.dnd.borrow_mut();
let serial = AsyncRequestSerial::get();
let type_ = type_
.cast_ref::<SelectionType>()
.or_else(|| dnd.find_type_by_hint(type_.hint()?))
.cloned()
.ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?;
let new_convert_selection = {
let Some(state) = dnd.state_mut() else {
return Err(RequestError::Ignored);
};
if state.transfer_id != id {
return Err(RequestError::NotSupported(NotSupportedError::new(
"Unknown data transfer",
)));
}
// If it's non-empty, assume that we're still waiting on some other fetch operation.
// The `SelectionNotify` handler will send a new `convert_selection` event if any
// more are on the stack.
let should_emit_convert_selection = state.pending_fetch_types.is_empty();
let atom = type_.atom();
state.pending_fetch_types.push_back((serial, type_));
should_emit_convert_selection.then_some((
state.target_window,
self.xconn.timestamp(),
atom,
))
};
if let Some((window, time, new_type)) = new_convert_selection {
// This results in the `SelectionNotify` event
dnd.convert_selection(window, time, new_type);
}
Ok(serial)
}
fn set_valid_dnd_actions(
&self,
id: DataTransferId,
actions: &[DndAction],
) -> Result<(), RequestError> {
let mut dnd = self.dnd.borrow_mut();
let Some(state) = dnd.state_mut() else {
return Err(os_error!(UnknownDataTransfer(id)).into());
};
if state.transfer_id != id {
return Err(os_error!(UnknownDataTransfer(id)).into());
}
state.accepted = !actions.is_empty();
Ok(())
}
}
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
@@ -767,6 +854,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop {
}
}
/// An operation was attempted on a data transfer ID, but that ID was invalid.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct UnknownDataTransfer(pub DataTransferId);
impl fmt::Display for UnknownDataTransfer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.0.into_raw();
write!(f, "Unknown data transfer with ID {id}")
}
}
impl std::error::Error for UnknownDataTransfer {}
pub(crate) struct DeviceInfo<'a> {
xconn: &'a XConnection,
info: *const ffi::XIDeviceInfo,

View File

@@ -1,10 +1,12 @@
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, VecDeque};
use std::mem::MaybeUninit;
use std::os::raw::{c_char, c_int, c_long, c_ulong};
use std::slice;
use std::sync::{Arc, Mutex};
use dpi::{PhysicalPosition, PhysicalSize};
use tracing::warn;
use winit_common::xkb::{self, Context, XkbState};
use winit_core::application::ApplicationHandler;
use winit_core::event::{
@@ -12,6 +14,7 @@ use winit_core::event::{
MouseScrollDelta, PointerKind, PointerSource, RawKeyEvent, SurfaceSizeWriter, TouchPhase,
WindowEvent,
};
use winit_core::event_loop::DndAction;
use winit_core::keyboard::ModifiersState;
use winit_core::window::WindowId;
use x11_dl::xinput2::{
@@ -31,7 +34,7 @@ use x11rb::x11_utils::{ExtensionInformation, Serialize};
use xkbcommon_dl::xkb_mod_mask_t;
use crate::atoms::*;
use crate::dnd::{Dnd, DndState};
use crate::dnd::{DndState, SelectionType};
use crate::event_loop::{
ALL_DEVICES, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType,
ScrollOrientation, mkdid, mkwid,
@@ -49,7 +52,6 @@ const KEYCODE_OFFSET: u8 = 8;
#[derive(Debug)]
pub struct EventProcessor {
pub dnd: Dnd,
pub ime_receiver: ImeReceiver,
pub ime_event_receiver: ImeEventReceiver,
pub randr_event_offset: u8,
@@ -294,7 +296,10 @@ impl EventProcessor {
unsafe { (self.target.xconn.xlib.XPending)(self.target.xconn.display) != 0 }
}
pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool {
pub fn poll_one_event<'a>(
&mut self,
event_ptr: &'a mut MaybeUninit<XEvent>,
) -> Option<&'a mut XEvent> {
// This function is used to poll and remove a single event
// from the Xlib event queue in a non-blocking, atomic way.
// XCheckIfEvent is non-blocking and removes events from queue.
@@ -304,20 +309,21 @@ impl EventProcessor {
unsafe extern "C" fn predicate(
_display: *mut XDisplay,
_event: *mut XEvent,
_arg: *mut c_char,
_filter: *mut c_char,
) -> c_int {
// This predicate always returns "true" (1) to accept all events
1
}
unsafe {
let event_initialized = unsafe {
(self.target.xconn.xlib.XCheckIfEvent)(
self.target.xconn.display,
event_ptr,
event_ptr.as_mut_ptr(),
Some(predicate),
std::ptr::null_mut(),
) != 0
}
};
event_initialized.then(|| unsafe { event_ptr.assume_init_mut() })
}
pub fn init_device(&self, device: xinput::DeviceId) {
@@ -423,21 +429,41 @@ impl EventProcessor {
}
if xev.message_type == atoms[XdndEnter] as c_ulong {
// 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;
self.dnd.version = Some(version);
let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1;
if !has_more_types {
let type_list = vec![
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,
];
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);
}
]
.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,31 +490,17 @@ 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);
// Action is specified in versions 2 and up, though we don't need it anyway.
// let action = xev.data.get_long(4);
let accepted = if let Some(ref type_list) = self.dnd.type_list {
type_list.contains(&atoms[TextUriList])
} else {
false
};
if !accepted {
unsafe {
self.dnd
.send_status(window, source_window, DndState::Rejected)
.expect("Failed to send `XdndStatus` message.");
}
self.dnd.reset();
// 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;
self.dnd.source_window = Some(source_window);
let time = if version == 0 {
// In version 0, time isn't specified
x11rb::CURRENT_TIME
@@ -499,86 +511,183 @@ impl EventProcessor {
// Log this timestamp.
self.target.xconn.set_timestamp(time);
// This results in the `SelectionNotify` event below
unsafe {
self.dnd.convert_selection(window, time);
}
unsafe {
self.dnd
.send_status(window, source_window, DndState::Accepted)
dnd.send_status(
window,
source_window,
if state.accepted { DndState::Accepted } else { DndState::Rejected },
)
.expect("Failed to send `XdndStatus` message.");
}
state.transfer_id
};
app.window_event(&self.target, window_id, WindowEvent::DragPosition {
id: transfer_id,
position: PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64),
// `Copy` is the default. Other actions are possible in X11, but the specification
// does not properly explain how to implement them (only giving a vague description
// of `XdndMove`). For simplicity's sake, we simply do not implement non-copy drag
// on X11.
// See https://www.freedesktop.org/wiki/Specifications/XDND/
proposed_action: Some(DndAction::Copy),
});
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,
let (source_window, transfer_id) = {
let dnd = self.target.dnd.borrow();
let Some(state) = dnd.state() else {
warn!("Received `XdndDrop` without `XdndEnter`");
return;
};
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 = state.source_window;
(source_window, state.transfer_id)
};
app.window_event(
&self.target,
window_id,
// TODO
WindowEvent::DragDropped {
id: transfer_id,
// `Copy` is the default. Other actions are possible in X11, but the
// specification does not properly explain how to implement
// them (only giving a vague description of `XdndMove`). For
// simplicity's sake, we simply do not implement non-copy drag
// on X11.
// See https://www.freedesktop.org/wiki/Specifications/XDND/
proposed_action: Some(DndAction::Copy),
},
);
let mut dnd = self.target.dnd.borrow_mut();
if let Some(state) =
dnd.state_mut().filter(|state| !state.pending_fetch_types.is_empty())
{
state.finished = Some((window, source_window));
} else {
unsafe {
self.dnd
.send_finished(window, source_window, state)
dnd.send_finished(window, source_window)
.expect("Failed to send `XdndFinished` message.");
}
}
self.dnd.reset();
return;
}
if xev.message_type == atoms[XdndLeave] as c_ulong {
if self.dnd.dragging {
let event = WindowEvent::DragLeft { position: Some(self.dnd.position) };
app.window_event(&self.target, window_id, event);
}
self.dnd.reset();
let dnd = self.target.dnd.borrow();
let Some(state) = dnd.state() else {
return;
};
app.window_event(&self.target, window_id, WindowEvent::DragLeft {
id: state.transfer_id,
});
}
}
fn selection_notify(&mut self, xev: &XSelectionEvent, app: &mut dyn ApplicationHandler) {
let atoms = self.target.xconn.atoms();
let window = xev.requestor as xproto::Window;
let window_id = mkwid(window);
let xwindow = xev.requestor as xproto::Window;
// Set the timestamp.
self.target.xconn.set_timestamp(xev.time as xproto::Timestamp);
// For now, winit only supports selections for drag-and-drop. This should be changed
// when clipboard support is implemented.
if xev.property != atoms[XdndSelection] as c_ulong {
return;
}
// This is where we receive data from drag and drop
self.dnd.result = None;
if let Ok(mut data) = unsafe { self.dnd.read_data(window) } {
let parse_result = self.dnd.parse_data(&mut data);
if let Ok(ref path_list) = parse_result {
let event = if self.dnd.dragging {
WindowEvent::DragMoved { position: self.dnd.position }
} else {
let paths = path_list.iter().map(Into::into).collect();
self.dnd.dragging = true;
WindowEvent::DragEntered { paths, position: self.dnd.position }
let (transfer_id, serial, type_) = {
let Some(state) = self.target.dnd.get_mut().state_mut() else {
return;
};
app.window_event(&self.target, window_id, event);
let Some((serial, type_)) = state.pending_fetch_types.pop_front() else {
return;
};
// Annoyingly, `xproto::Atom` and `x11_dl::Atom` are different on 64-bit
// but the same on 32-bit, so just casting to `u32` will cause "casting
// to same type" clippy warnings when compiled as 32-bit.
#[cfg(target_pointer_width = "32")]
let target_type = xev.target;
#[cfg(target_pointer_width = "64")]
let target_type = xev.target as u32;
if target_type != type_.atom() {
let get_name = |atom| {
self.target
.xconn
.xcb_connection()
.get_atom_name(atom)
.ok()
.and_then(|cookie| cookie.reply().ok())
.and_then(|reply| String::from_utf8(reply.name).ok())
};
let expected_type_name = get_name(type_.atom());
let found_type_name = get_name(xev.type_ as _);
let extra_context = match (expected_type_name, found_type_name) {
(Some(expected), Some(found)) => {
format!(" (expected {expected}, found {found})")
},
(Some(expected), None) => format!(" (expected {expected})"),
(None, Some(found)) => format!(" (found {found})"),
(None, None) => "".to_string(),
};
warn!(
"Received `SelectionNotify` with unexpected type{extra_context}. Continuing, \
but this may be a bug."
);
}
self.dnd.result = Some(parse_result);
(state.transfer_id, serial, type_)
};
let value = match self.target.dnd.borrow().read_data(xwindow, type_) {
Ok(value) => Arc::new(value),
Err(err) => {
warn!("Failed to read selection: {err}");
return;
},
};
let window_id = mkwid(xwindow);
app.window_event(&self.target, window_id, WindowEvent::DataTransferReceived {
id: transfer_id,
serial,
value,
});
let dnd = self.target.dnd.borrow();
// If we have another fetch pending, request it from the drag source window
if let Some((window, type_)) = dnd.state().and_then(|state| {
state
.pending_fetch_types
.front()
.cloned()
.map(|(_, type_)| (state.target_window, type_))
}) {
dnd.convert_selection(window, self.target.xconn.timestamp(), type_.atom());
} else if let Some((this_window, target_window)) =
dnd.state().and_then(|state| state.finished)
{
unsafe {
dnd.send_finished(this_window, target_window)
.expect("Failed to send `XdndFinished` message.");
}
}
}

View File

@@ -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)]

View File

@@ -17,6 +17,7 @@ pub enum GetPropertyError {
X11rbError(Arc<ReplyError>),
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"),
}
}
}

View File

@@ -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,

View File

@@ -1,7 +1,14 @@
use cfg_aliases::cfg_aliases;
// Only relevant for examples and Winit, our usage of println! is fine here.
#[allow(clippy::disallowed_macros)]
#[allow(
clippy::disallowed_macros,
reason = "Only relevant for examples and Winit, our usage of println! is fine here."
)]
#[allow(
semicolon_in_expressions_from_macros,
reason = "This is a future incompatibility lint and we currently use cfg_aliases 0.2.1, which \
has not been updated to resolve the latest lints"
)]
fn main() {
// Dummy invocation to enable change-tracking in build scripts.
println!("cargo:rerun-if-changed=build.rs");

View File

@@ -1,10 +1,18 @@
use std::error::Error;
use std::path::PathBuf;
use std::sync::Arc;
use image::imageops::FilterType;
use image::{DynamicImage, GenericImageView, RgbImage};
use softbuffer::{Context, Surface};
use tracing::info;
use tracing::{error, info, warn};
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
use winit::data_transfer::{DataTransferId, DataTransferSendBuilder, SendData, TypeHint};
use winit::event::{ButtonSource, MouseButton, WindowEvent};
use winit::event_loop::{
ActiveEventLoop, AsyncRequestSerial, DndAction, DragIcon, EventLoop, OwnedDisplayHandle,
};
use winit::icon::{Icon, RgbaIcon};
use winit::window::{Window, WindowAttributes, WindowId};
#[path = "util/fill.rs"]
@@ -17,20 +25,55 @@ fn main() -> Result<(), Box<dyn Error>> {
let event_loop = EventLoop::new()?;
let app = Application::default();
let app = Application::new();
Ok(event_loop.run_app(app)?)
}
/// Application state and event handling.
#[derive(Default, Debug)]
#[derive(Debug)]
struct Application {
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
last_dnd_fetch: Option<AsyncRequestSerial>,
last_drag_start: Option<DataTransferId>,
drag_icon: (Icon, i32, i32),
drag_image_data: Arc<RgbImage>,
}
const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png");
impl Application {
fn new() -> Self {
let drag_icon = load_icon(DRAG_IMAGE);
let drag_image_data = Arc::new(image::load_from_memory(DRAG_IMAGE).unwrap().into_rgb8());
Self {
surface: None,
last_dnd_fetch: None,
last_drag_start: None,
drag_icon,
drag_image_data,
}
}
}
fn load_icon(bytes: &[u8]) -> (Icon, i32, i32) {
let (icon_rgba, icon_width, icon_height) = {
let image = image::load_from_memory(bytes).unwrap().into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
(
RgbaIcon::new(icon_rgba, icon_width, icon_height).expect("Failed to open icon").into(),
-(icon_width as i32) / 2,
-(icon_height as i32) / 2,
)
}
impl ApplicationHandler for Application {
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
let window_attributes =
WindowAttributes::default().with_title("Drag and drop files on me!");
WindowAttributes::default().with_title("Drag and drop files, text or HTML onto me!");
let window = event_loop.create_window(window_attributes).unwrap();
let context = Context::new(event_loop.owned_display_handle()).unwrap();
let surface = Surface::new(&context, window).unwrap();
@@ -40,14 +83,168 @@ impl ApplicationHandler for Application {
fn window_event(
&mut self,
event_loop: &dyn ActiveEventLoop,
_window_id: WindowId,
window_id: WindowId,
event: WindowEvent,
) {
match event {
WindowEvent::DragLeft { .. }
| WindowEvent::DragEntered { .. }
| WindowEvent::DragMoved { .. }
| WindowEvent::DragDropped { .. } => {
WindowEvent::PointerButton { button: ButtonSource::Mouse(button), state, .. }
if button == MouseButton::Left && state.is_pressed() =>
{
let (icon, offset_x, offset_y) = self.drag_icon.clone();
// In a real application, you probably wouldn't advertise so many types.
// Depending on platform and destination application, different options may be
// chosen.
let result = event_loop.start_drag(
window_id,
DataTransferSendBuilder::new(self.drag_image_data.clone())
.with_type(TypeHint::UriList, |_, _| {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let root = manifest_dir.parent().unwrap();
let this_file = root.join(file!());
let icon_file = this_file.parent().unwrap().join("data/icon.png");
SendData::from_file_paths([icon_file])
})
.with_type(TypeHint::Plaintext, |_, _| Some("Winit example".to_string()))
.with_type(TypeHint::Html, |_, _| {
Some("<span><strong>Winit</strong> example</span>".to_string())
})
// You can advertise a `TypeHint` that can match many types, and switch
// inside the callback. For example, this will match any image type.
// This may be desirable on some platforms which restrict the set of
// image types that can be sent.
.with_type(TypeHint::Image { extension_hint: None }, |image, ty| {
let hint = ty.hint()?;
match hint {
TypeHint::Image { extension_hint } => {
let image = DynamicImage::from((**image).clone());
let (w, h) = image.dimensions();
let image = image
.resize(w * 8, h * 8, FilterType::Gaussian)
.into_rgb8();
let ext = extension_hint.unwrap_or("png");
info!("Destination requested image as {ext}, converting...");
let format = image::ImageFormat::from_extension(ext)?;
let mut out_buf = Vec::new();
let mut out_writer = std::io::Cursor::new(&mut out_buf);
image.write_to(&mut out_writer, format).ok()?;
Some(out_buf)
},
_ => None,
}
})
.build(),
&[DndAction::Move, DndAction::Copy],
Some(DragIcon { icon, offset_x, offset_y }),
);
self.last_drag_start = result.ok();
},
WindowEvent::DragLeft { .. } => {
info!("{event:?}");
},
WindowEvent::DataTransferReceived { ref value, serial, .. } => {
assert_eq!(self.last_dnd_fetch, Some(serial));
match value.type_().hint() {
Some(TypeHint::Plaintext | TypeHint::Html) => {
let Ok(text) = value.try_as_string() else {
return;
};
info!("{text:?}");
},
Some(TypeHint::UriList) => {
let Ok(uris) = value.try_as_uris() else {
return;
};
info!("URIs: {uris:#?}");
// If you only want to support dropping files, rather than arbitrary URIs,
// you can use the `try_as_file_paths` helper method.
let Ok(uris_as_paths) = value.try_as_file_paths() else {
return;
};
info!("URIs as file paths: {uris_as_paths:#?}");
},
Some(TypeHint::Image { extension_hint: ext }) => {
let Ok(bytes) = value.try_as_bytes() else {
return;
};
let format = ext.and_then(image::ImageFormat::from_extension);
let reader = std::io::Cursor::new(&bytes[..]);
let reader = match format {
Some(fmt) => image::ImageReader::with_format(reader, fmt),
None => image::ImageReader::new(reader),
};
match reader.decode() {
Ok(image) => {
let width = image.width();
let height = image.height();
info!("Received image ({width}x{height})");
},
Err(err) => {
warn!("Failed to decode image: {err}");
},
}
},
_ => {
unreachable!("Received a type we didn't ask for!");
},
}
},
WindowEvent::DragPosition { .. } => {
info!("{event:?}");
},
WindowEvent::DragDropped { .. } => {
info!("{event:?}");
},
WindowEvent::DragEntered { id, .. } => {
info!("{event:?}");
let data_transfer = match event_loop.data_transfer(id) {
Ok(dt) => dt,
Err(e) => {
error!("{e}");
return;
},
};
info!("Types: {:#?}", data_transfer.available_types());
let readable_image_types = image::ImageFormat::all()
.filter(|fmt| fmt.reading_enabled())
.filter_map(|fmt| {
let ext = fmt.extensions_str().first()?;
Some(TypeHint::Image { extension_hint: Some(ext) })
});
let mut valid_types = readable_image_types.chain([
TypeHint::Html,
TypeHint::UriList,
TypeHint::Plaintext,
]);
let valid_type = valid_types.find(|ty| data_transfer.has_type(ty));
let Some(type_) = valid_type else {
event_loop.set_valid_dnd_actions(id, &[]).unwrap();
return;
};
event_loop.set_valid_dnd_actions(id, &[DndAction::Move, DndAction::Copy]).unwrap();
self.last_dnd_fetch = event_loop.fetch_data_transfer(id, &type_).ok();
},
WindowEvent::OutgoingDragDropped { .. } => {
info!("{event:?}");
},
WindowEvent::OutgoingDragCanceled { .. } => {
info!("{event:?}");
},
WindowEvent::RedrawRequested => {

View File

@@ -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;