mirror of
https://github.com/rust-windowing/winit.git
synced 2026-08-31 05:40:04 -04:00
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:
@@ -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`
|
||||
|
||||
@@ -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
499
winit-appkit/src/dnd.rs
Normal 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_)
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -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());
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user