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

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