mirror of
https://github.com/rust-windowing/winit.git
synced 2026-08-29 04: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:
@@ -36,6 +36,7 @@ rwh_06 = { package = "raw-window-handle", version = "0.6", features = ["std"] }
|
|||||||
serde = { version = "1", features = ["serde_derive"] }
|
serde = { version = "1", features = ["serde_derive"] }
|
||||||
smol_str = "0.3"
|
smol_str = "0.3"
|
||||||
tracing = { version = "0.1.40", default-features = false }
|
tracing = { version = "0.1.40", default-features = false }
|
||||||
|
url = "2"
|
||||||
|
|
||||||
# Dev dependencies.
|
# Dev dependencies.
|
||||||
image = { version = "0.25.0", default-features = false }
|
image = { version = "0.25.0", default-features = false }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ TME_LEAVE = "TME_LEAVE" # From windows_sys::Win32::UI::Input::Keyboa
|
|||||||
XF86_Calculater = "XF86_Calculater" # From xkbcommon_dl::keysyms::XF86_Calculater
|
XF86_Calculater = "XF86_Calculater" # From xkbcommon_dl::keysyms::XF86_Calculater
|
||||||
ptd = "ptd" # From windows_sys::Win32::System::Com::FORMATETC { ptd, ..}
|
ptd = "ptd" # From windows_sys::Win32::System::Com::FORMATETC { ptd, ..}
|
||||||
requestor = "requestor" # From x11_dl::xlib::XSelectionEvent { requestor ..}
|
requestor = "requestor" # From x11_dl::xlib::XSelectionEvent { requestor ..}
|
||||||
|
unknwn = "unknwn" # Windows SDK header filename `unknwn.h`
|
||||||
|
|
||||||
[files]
|
[files]
|
||||||
extend-exclude = ["*.drawio"]
|
extend-exclude = ["*.drawio"]
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ objc2-app-kit = { workspace = true, features = [
|
|||||||
"NSControl",
|
"NSControl",
|
||||||
"NSCursor",
|
"NSCursor",
|
||||||
"NSDragging",
|
"NSDragging",
|
||||||
|
"NSDraggingItem",
|
||||||
|
"NSDraggingSession",
|
||||||
"NSEvent",
|
"NSEvent",
|
||||||
"NSGraphics",
|
"NSGraphics",
|
||||||
"NSGraphicsContext",
|
"NSGraphicsContext",
|
||||||
@@ -46,6 +48,7 @@ objc2-app-kit = { workspace = true, features = [
|
|||||||
"NSOpenGLView",
|
"NSOpenGLView",
|
||||||
"NSPanel",
|
"NSPanel",
|
||||||
"NSPasteboard",
|
"NSPasteboard",
|
||||||
|
"NSPasteboardItem",
|
||||||
"NSResponder",
|
"NSResponder",
|
||||||
"NSRunningApplication",
|
"NSRunningApplication",
|
||||||
"NSScreen",
|
"NSScreen",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::cell::{Cell, OnceCell, RefCell};
|
use std::cell::{Cell, OnceCell, RefCell};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -6,22 +7,30 @@ use std::time::Instant;
|
|||||||
|
|
||||||
use dispatch2::MainThreadBound;
|
use dispatch2::MainThreadBound;
|
||||||
use objc2::MainThreadMarker;
|
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 objc2_foundation::NSNotification;
|
||||||
use winit_common::core_foundation::{EventLoopProxy, MainRunLoop};
|
use winit_common::core_foundation::{EventLoopProxy, MainRunLoop};
|
||||||
use winit_common::event_handler::EventHandler;
|
use winit_common::event_handler::EventHandler;
|
||||||
use winit_core::application::ApplicationHandler;
|
use winit_core::application::ApplicationHandler;
|
||||||
|
use winit_core::data_transfer::DataTransferId;
|
||||||
use winit_core::event::{StartCause, WindowEvent};
|
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 winit_core::window::WindowId;
|
||||||
|
|
||||||
use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately};
|
use super::event_loop::{ActiveEventLoop, notify_windows_of_exit, stop_app_immediately};
|
||||||
use super::menu;
|
use super::menu;
|
||||||
use super::observer::EventLoopWaker;
|
use super::observer::EventLoopWaker;
|
||||||
|
use crate::dnd::Pasteboards;
|
||||||
|
use crate::window_delegate::WindowDelegate;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) struct AppState {
|
pub(super) struct AppState {
|
||||||
mtm: MainThreadMarker,
|
mtm: MainThreadMarker,
|
||||||
|
drag_state: RefCell<Option<DragState>>,
|
||||||
|
pasteboards: Pasteboards,
|
||||||
activation_policy: Option<NSApplicationActivationPolicy>,
|
activation_policy: Option<NSApplicationActivationPolicy>,
|
||||||
default_menu: bool,
|
default_menu: bool,
|
||||||
activate_ignoring_other_apps: bool,
|
activate_ignoring_other_apps: bool,
|
||||||
@@ -43,10 +52,17 @@ pub(super) struct AppState {
|
|||||||
start_time: Cell<Option<Instant>>,
|
start_time: Cell<Option<Instant>>,
|
||||||
wait_timeout: Cell<Option<Instant>>,
|
wait_timeout: Cell<Option<Instant>>,
|
||||||
pending_redraw: RefCell<Vec<WindowId>>,
|
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
|
// 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.
|
// 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
|
// SAFETY: Creating `MainThreadBound` in a `const` context, where there is no concept of the
|
||||||
// main thread.
|
// main thread.
|
||||||
static GLOBAL: MainThreadBound<OnceCell<Rc<AppState>>> =
|
static GLOBAL: MainThreadBound<OnceCell<Rc<AppState>>> =
|
||||||
@@ -65,6 +81,8 @@ impl AppState {
|
|||||||
|
|
||||||
let this = Rc::new(Self {
|
let this = Rc::new(Self {
|
||||||
mtm,
|
mtm,
|
||||||
|
pasteboards: Default::default(),
|
||||||
|
drag_state: Default::default(),
|
||||||
activation_policy,
|
activation_policy,
|
||||||
default_menu,
|
default_menu,
|
||||||
activate_ignoring_other_apps,
|
activate_ignoring_other_apps,
|
||||||
@@ -82,6 +100,7 @@ impl AppState {
|
|||||||
waker: RefCell::new(EventLoopWaker::new()),
|
waker: RefCell::new(EventLoopWaker::new()),
|
||||||
start_time: Cell::new(None),
|
start_time: Cell::new(None),
|
||||||
wait_timeout: Cell::new(None),
|
wait_timeout: Cell::new(None),
|
||||||
|
windows: Default::default(),
|
||||||
pending_redraw: RefCell::new(vec![]),
|
pending_redraw: RefCell::new(vec![]),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,6 +115,20 @@ impl AppState {
|
|||||||
.clone()
|
.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,
|
// NOTE: This notification will, globally, only be emitted once,
|
||||||
// no matter how many `EventLoop`s the user creates.
|
// no matter how many `EventLoop`s the user creates.
|
||||||
pub fn did_finish_launching(self: &Rc<Self>, _notification: &NSNotification) {
|
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));
|
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`
|
/// 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::cursor::{CursorIcon, CursorImage, CustomCursorProvider, CustomCursorSource};
|
||||||
use winit_core::error::{NotSupportedError, RequestError};
|
use winit_core::error::{NotSupportedError, RequestError};
|
||||||
|
use winit_core::icon::{Icon, RgbaIcon};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct CustomCursor(pub(crate) Retained<NSCursor>);
|
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> {
|
pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Result<Retained<NSCursor>, RequestError> {
|
||||||
let width = cursor.width();
|
let width = cursor.width();
|
||||||
let height = cursor.height();
|
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::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use objc2::rc::{Retained, autoreleasepool};
|
use objc2::rc::{Retained, autoreleasepool};
|
||||||
use objc2::runtime::ProtocolObject;
|
use objc2::runtime::ProtocolObject;
|
||||||
use objc2::{MainThreadMarker, available};
|
use objc2::{AnyThread, MainThreadMarker, available};
|
||||||
use objc2_app_kit::{
|
use objc2_app_kit::{
|
||||||
NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification,
|
NSApplication, NSApplicationActivationPolicy, NSApplicationDidFinishLaunchingNotification,
|
||||||
NSApplicationWillTerminateNotification, NSWindow,
|
NSApplicationWillTerminateNotification, NSDraggingItem, NSWindow,
|
||||||
};
|
};
|
||||||
use objc2_core_foundation::{CFIndex, CFRunLoopActivity, kCFRunLoopCommonModes};
|
use objc2_core_foundation::{
|
||||||
use objc2_foundation::{NSNotificationCenter, NSObjectProtocol};
|
CFIndex, CFRunLoopActivity, CGPoint, CGRect, CGSize, kCFRunLoopCommonModes,
|
||||||
|
};
|
||||||
|
use objc2_foundation::{NSArray, NSNotificationCenter, NSObjectProtocol, NSString};
|
||||||
use rwh_06::HasDisplayHandle;
|
use rwh_06::HasDisplayHandle;
|
||||||
use tracing::debug_span;
|
use tracing::debug_span;
|
||||||
use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers};
|
use winit_common::core_foundation::{MainRunLoop, MainRunLoopObserver, tracing_observers};
|
||||||
use winit_common::foundation::create_observer;
|
use winit_common::foundation::create_observer;
|
||||||
use winit_core::application::ApplicationHandler;
|
use winit_core::application::ApplicationHandler;
|
||||||
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
|
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::error::{EventLoopError, RequestError};
|
||||||
|
use winit_core::event::WindowEvent;
|
||||||
use winit_core::event_loop::pump_events::PumpStatus;
|
use winit_core::event_loop::pump_events::PumpStatus;
|
||||||
use winit_core::event_loop::{
|
use winit_core::event_loop::{
|
||||||
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
|
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
|
||||||
EventLoopProxy as CoreEventLoopProxy, OwnedDisplayHandle as CoreOwnedDisplayHandle,
|
DndAction, DragIcon, EventLoopProxy as CoreEventLoopProxy,
|
||||||
|
OwnedDisplayHandle as CoreOwnedDisplayHandle,
|
||||||
};
|
};
|
||||||
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
|
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::override_send_event;
|
||||||
use super::app_state::AppState;
|
use super::app_state::AppState;
|
||||||
@@ -32,6 +40,8 @@ use super::cursor::CustomCursor;
|
|||||||
use super::event::dummy_event;
|
use super::event::dummy_event;
|
||||||
use super::monitor;
|
use super::monitor;
|
||||||
use crate::ActivationPolicy;
|
use crate::ActivationPolicy;
|
||||||
|
use crate::cursor::image_from_icon;
|
||||||
|
use crate::dnd::{PasteboardTypeSpec, PasteboardWriter, dnd_actions_to_ns_drag_operation};
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -126,8 +136,183 @@ impl RootActiveEventLoop for ActiveEventLoop {
|
|||||||
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
|
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
|
||||||
self
|
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 {
|
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
|
||||||
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
|
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
|
||||||
let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new());
|
let raw = rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new());
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ mod util;
|
|||||||
mod app;
|
mod app;
|
||||||
mod app_state;
|
mod app_state;
|
||||||
mod cursor;
|
mod cursor;
|
||||||
|
mod dnd;
|
||||||
mod event;
|
mod event;
|
||||||
mod event_loop;
|
mod event_loop;
|
||||||
mod ffi;
|
mod ffi;
|
||||||
@@ -91,6 +92,7 @@ use winit_core::event_loop::ActiveEventLoop;
|
|||||||
use winit_core::monitor::MonitorHandle;
|
use winit_core::monitor::MonitorHandle;
|
||||||
use winit_core::window::{PlatformWindowAttributes, Window};
|
use winit_core::window::{PlatformWindowAttributes, Window};
|
||||||
|
|
||||||
|
pub use self::dnd::{Pasteboard, PasteboardType, PasteboardValue};
|
||||||
pub use self::event::{physicalkey_to_scancode, scancode_to_physicalkey};
|
pub use self::event::{physicalkey_to_scancode, scancode_to_physicalkey};
|
||||||
use self::event_loop::ActiveEventLoop as AppKitActiveEventLoop;
|
use self::event_loop::ActiveEventLoop as AppKitActiveEventLoop;
|
||||||
pub use self::event_loop::{EventLoop, PlatformSpecificEventLoopAttributes};
|
pub use self::event_loop::{EventLoop, PlatformSpecificEventLoopAttributes};
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ use objc2::rc::Retained;
|
|||||||
use objc2::runtime::{AnyObject, Sel};
|
use objc2::runtime::{AnyObject, Sel};
|
||||||
use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send};
|
use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send};
|
||||||
use objc2_app_kit::{
|
use objc2_app_kit::{
|
||||||
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea,
|
NSApplication, NSCursor, NSDragOperation, NSDraggingSession, NSEvent, NSEventPhase,
|
||||||
NSTrackingAreaOptions, NSView, NSViewLayerContentsRedrawPolicy, NSWindow,
|
NSResponder, NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView,
|
||||||
|
NSViewLayerContentsRedrawPolicy, NSWindow,
|
||||||
};
|
};
|
||||||
use objc2_core_foundation::CGRect;
|
use objc2_core_foundation::CGRect;
|
||||||
use objc2_foundation::{
|
use objc2_foundation::{
|
||||||
@@ -114,6 +115,12 @@ pub struct ViewState {
|
|||||||
/// Strong reference to the global application state.
|
/// Strong reference to the global application state.
|
||||||
app_state: Rc<AppState>,
|
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>,
|
cursor_state: RefCell<CursorState>,
|
||||||
ime_position: Cell<NSPoint>,
|
ime_position: Cell<NSPoint>,
|
||||||
ime_size: Cell<NSSize>,
|
ime_size: Cell<NSSize>,
|
||||||
@@ -789,6 +796,8 @@ impl WinitView {
|
|||||||
) -> Retained<Self> {
|
) -> Retained<Self> {
|
||||||
let this = mtm.alloc().set_ivars(ViewState {
|
let this = mtm.alloc().set_ivars(ViewState {
|
||||||
app_state: Rc::clone(app_state),
|
app_state: Rc::clone(app_state),
|
||||||
|
dragging_session: Default::default(),
|
||||||
|
drag_operations: Cell::new(NSDragOperation::empty()),
|
||||||
cursor_state: Default::default(),
|
cursor_state: Default::default(),
|
||||||
ime_position: Default::default(),
|
ime_position: Default::default(),
|
||||||
ime_size: 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
|
self.window().backingScaleFactor() as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1117,6 +1126,29 @@ impl WinitView {
|
|||||||
self.queue_event(WindowEvent::ModifiersChanged(self.ivars().modifiers.get()));
|
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) {
|
fn mouse_click(&self, event: &NSEvent, button_state: ElementState) {
|
||||||
let position = self.mouse_view_point(event).to_physical(self.scale_factor());
|
let position = self.mouse_view_point(event).to_physical(self.scale_factor());
|
||||||
let button = mouse_button(event);
|
let button = mouse_button(event);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ impl Window {
|
|||||||
let mtm = window_target.mtm;
|
let mtm = window_target.mtm;
|
||||||
let delegate =
|
let delegate =
|
||||||
autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?;
|
autoreleasepool(|_| WindowDelegate::new(&window_target.app_state, attributes, mtm))?;
|
||||||
|
window_target.app_state.register_window(&delegate, mtm);
|
||||||
Ok(Window {
|
Ok(Window {
|
||||||
window: MainThreadBound::new(delegate.window().retain(), mtm),
|
window: MainThreadBound::new(delegate.window().retain(), mtm),
|
||||||
delegate: MainThreadBound::new(delegate, mtm),
|
delegate: MainThreadBound::new(delegate, mtm),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::ptr;
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use dispatch2::MainThreadBound;
|
||||||
use dpi::{
|
use dpi::{
|
||||||
LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize,
|
LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize,
|
||||||
Position, Size,
|
Position, Size,
|
||||||
@@ -19,14 +20,14 @@ use objc2::{
|
|||||||
use objc2_app_kit::{
|
use objc2_app_kit::{
|
||||||
NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization,
|
NSAppKitVersionNumber, NSAppKitVersionNumber10_12, NSAppearance, NSAppearanceCustomization,
|
||||||
NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType,
|
NSAppearanceNameAqua, NSApplication, NSApplicationPresentationOptions, NSBackingStoreType,
|
||||||
NSColor, NSDraggingDestination, NSDraggingInfo, NSRequestUserAttentionType, NSScreen,
|
NSColor, NSDragOperation, NSDraggingContext, NSDraggingDestination, NSDraggingInfo,
|
||||||
NSToolbar, NSView, NSViewFrameDidChangeNotification, NSWindow, NSWindowButton,
|
NSDraggingSession, NSDraggingSource, NSPasteboardTypeFileURL, NSPasteboardTypeHTML,
|
||||||
NSWindowDelegate, NSWindowLevel, NSWindowOcclusionState, NSWindowOrderingMode,
|
NSPasteboardTypePNG, NSPasteboardTypeSound, NSPasteboardTypeString, NSPasteboardTypeTIFF,
|
||||||
NSWindowSharingType, NSWindowStyleMask, NSWindowTabbingMode, NSWindowTitleVisibility,
|
NSRequestUserAttentionType, NSScreen, NSToolbar, NSView, NSViewFrameDidChangeNotification,
|
||||||
NSWindowToolbarStyle,
|
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_foundation::{CGFloat, CGPoint};
|
||||||
use objc2_core_graphics::{
|
use objc2_core_graphics::{
|
||||||
CGAcquireDisplayFadeReservation, CGAssociateMouseAndMouseCursorPosition, CGDisplayCapture,
|
CGAcquireDisplayFadeReservation, CGAssociateMouseAndMouseCursorPosition, CGDisplayCapture,
|
||||||
@@ -44,6 +45,7 @@ use objc2_foundation::{
|
|||||||
use tracing::{debug_span, trace, warn};
|
use tracing::{debug_span, trace, warn};
|
||||||
use winit_common::core_foundation::MainRunLoop;
|
use winit_common::core_foundation::MainRunLoop;
|
||||||
use winit_core::cursor::Cursor;
|
use winit_core::cursor::Cursor;
|
||||||
|
use winit_core::data_transfer::DataTransferId;
|
||||||
use winit_core::error::{NotSupportedError, RequestError};
|
use winit_core::error::{NotSupportedError, RequestError};
|
||||||
use winit_core::event::{SurfaceSizeWriter, WindowEvent};
|
use winit_core::event::{SurfaceSizeWriter, WindowEvent};
|
||||||
use winit_core::icon::Icon;
|
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::util::cgerr;
|
||||||
use super::view::WinitView;
|
use super::view::WinitView;
|
||||||
use super::window::{WinitPanel, WinitWindow, window_id};
|
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};
|
use crate::{OptionAsAlt, WindowAttributesMacOS, WindowExtMacOS};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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 {
|
unsafe impl NSDraggingDestination for WindowDelegate {
|
||||||
/// Invoked when the dragged image enters destination bounds or frame
|
/// Invoked when the dragged image enters destination bounds or frame
|
||||||
#[unsafe(method(draggingEntered:))]
|
#[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();
|
let _entered = debug_span!("draggingEntered:").entered();
|
||||||
|
|
||||||
use std::path::PathBuf;
|
let pb =
|
||||||
|
MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap());
|
||||||
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 dl = sender.draggingLocation();
|
let dl = sender.draggingLocation();
|
||||||
let dl = self.view().convertPoint_fromView(dl, None);
|
let dl = self.view().convertPoint_fromView(dl, None);
|
||||||
let position =
|
let position =
|
||||||
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
|
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))]
|
#[unsafe(method(wantsPeriodicDraggingUpdates))]
|
||||||
@@ -400,17 +447,46 @@ define_class!(
|
|||||||
/// Invoked periodically as the image is held within the destination area, allowing
|
/// Invoked periodically as the image is held within the destination area, allowing
|
||||||
/// modification of the dragging operation or mouse-pointer position.
|
/// modification of the dragging operation or mouse-pointer position.
|
||||||
#[unsafe(method(draggingUpdated:))]
|
#[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 _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 = sender.draggingLocation();
|
||||||
let dl = self.view().convertPoint_fromView(dl, None);
|
let dl = self.view().convertPoint_fromView(dl, None);
|
||||||
let position =
|
let position =
|
||||||
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
|
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
|
/// Invoked when the image is released
|
||||||
@@ -425,30 +501,43 @@ define_class!(
|
|||||||
fn perform_drag_operation(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
|
fn perform_drag_operation(&self, sender: &ProtocolObject<dyn NSDraggingInfo>) -> bool {
|
||||||
let _entered = debug_span!("performDragOperation:").entered();
|
let _entered = debug_span!("performDragOperation:").entered();
|
||||||
|
|
||||||
use std::path::PathBuf;
|
let vars = self.ivars();
|
||||||
|
|
||||||
let pb = sender.draggingPasteboard();
|
let Some(transfer_id) =
|
||||||
|
vars.app_state.drag_state().borrow().as_ref().map(|state| state.id)
|
||||||
#[allow(deprecated)]
|
else {
|
||||||
let property_list = match pb.propertyListForType(unsafe { NSFilenamesPboardType }) {
|
return false.into();
|
||||||
Some(property_list) => property_list,
|
|
||||||
None => return false.into(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let paths = property_list
|
let pb =
|
||||||
.downcast::<NSArray>()
|
MainThreadBound::new(sender.draggingPasteboard(), MainThreadMarker::new().unwrap());
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
let source_operations = sender.draggingSourceOperationMask();
|
||||||
.map(|file| PathBuf::from(file.downcast::<NSString>().unwrap().to_string()))
|
// let operations = ns_drag_operation_to_dnd_actions(source_operations);
|
||||||
.collect();
|
|
||||||
|
vars.app_state.pasteboards().set_pasteboard(transfer_id, &pb);
|
||||||
|
|
||||||
let dl = sender.draggingLocation();
|
let dl = sender.draggingLocation();
|
||||||
let dl = self.view().convertPoint_fromView(dl, None);
|
let dl = self.view().convertPoint_fromView(dl, None);
|
||||||
let position =
|
let position =
|
||||||
LogicalPosition::<f64>::from((dl.x, dl.y)).to_physical(self.scale_factor());
|
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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,6 +545,10 @@ define_class!(
|
|||||||
#[unsafe(method(concludeDragOperation:))]
|
#[unsafe(method(concludeDragOperation:))]
|
||||||
fn conclude_drag_operation(&self, _sender: Option<&NSObject>) {
|
fn conclude_drag_operation(&self, _sender: Option<&NSObject>) {
|
||||||
let _entered = debug_span!("concludeDragOperation:").entered();
|
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
|
/// Invoked when the dragging operation is cancelled
|
||||||
@@ -463,13 +556,39 @@ define_class!(
|
|||||||
fn dragging_exited(&self, sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) {
|
fn dragging_exited(&self, sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) {
|
||||||
let _entered = debug_span!("draggingExited:").entered();
|
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 = sender.draggingLocation();
|
||||||
let dl = self.view().convertPoint_fromView(dl, None);
|
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 {
|
if macos_attrs.titlebar_buttons_hidden {
|
||||||
for titlebar_button in &[
|
for titlebar_button in &[
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
NSWindowFullScreenButton,
|
objc2_app_kit::NSWindowFullScreenButton,
|
||||||
NSWindowButton::MiniaturizeButton,
|
NSWindowButton::MiniaturizeButton,
|
||||||
NSWindowButton::CloseButton,
|
NSWindowButton::CloseButton,
|
||||||
NSWindowButton::ZoomButton,
|
NSWindowButton::ZoomButton,
|
||||||
@@ -756,10 +875,6 @@ fn new_window(
|
|||||||
window.setBackgroundColor(Some(&NSColor::clearColor()));
|
window.setBackgroundColor(Some(&NSColor::clearColor()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// register for drag and drop operations.
|
|
||||||
#[allow(deprecated)]
|
|
||||||
window.registerForDraggedTypes(&NSArray::from_slice(&[unsafe { NSFilenamesPboardType }]));
|
|
||||||
|
|
||||||
Some(window)
|
Some(window)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -837,6 +952,22 @@ impl WindowDelegate {
|
|||||||
|
|
||||||
window.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
|
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.
|
// Listen for theme change event.
|
||||||
//
|
//
|
||||||
// SAFETY: The observer is un-registered in the `Drop` of the delegate.
|
// SAFETY: The observer is un-registered in the `Drop` of the delegate.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ keyboard-types.workspace = true
|
|||||||
rwh_06.workspace = true
|
rwh_06.workspace = true
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
smol_str.workspace = true
|
smol_str.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
|
||||||
# `wasm32-unknown-unknown` and `wasm32-none`, but not `wasm32-wasi`.
|
# `wasm32-unknown-unknown` and `wasm32-none`, but not `wasm32-wasi`.
|
||||||
[target.'cfg(all(target_family = "wasm", any(target_os = "unknown", target_os = "none")))'.dependencies]
|
[target.'cfg(all(target_family = "wasm", any(target_os = "unknown", target_os = "none")))'.dependencies]
|
||||||
|
|||||||
593
winit-core/src/data_transfer.rs
Normal file
593
winit-core/src/data_transfer.rs
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
//! Cross-platform abstractions related to data transfer (i.e. clipboard and drag-and-drop).
|
||||||
|
//!
|
||||||
|
//! > **NOTE**: Interacting with the clipboard is currently not implemented in Winit, and
|
||||||
|
//! > this API is only used for drag-and-drop.
|
||||||
|
//!
|
||||||
|
//! # Quickstart
|
||||||
|
//!
|
||||||
|
//! The API in this module is used for both sending and receiving data. The flow is detailed below,
|
||||||
|
//! but to quickly get started, the relevant APIs are the following:
|
||||||
|
//!
|
||||||
|
//! ### Receiving a drag-and-drop operation
|
||||||
|
//!
|
||||||
|
//! - [`DragEntered`](crate::event::WindowEvent::DragEntered) - informs a window that a new drag
|
||||||
|
//! operation has started.
|
||||||
|
//! - [`data_transfer`](crate::event_loop::ActiveEventLoop::data_transfer) - get metadata about the
|
||||||
|
//! incoming transfer.
|
||||||
|
//! - [`DataTransfer`] - metadata about the incoming transfer, in particular the available types
|
||||||
|
//! - [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions) - the
|
||||||
|
//! application must set at least some actions as valid in order for the drag to be considered
|
||||||
|
//! accepted.
|
||||||
|
//! - [`fetch_data_transfer`](crate::event_loop::ActiveEventLoop::fetch_data_transfer) - request the
|
||||||
|
//! actual data, with a specific type, from the data transfer.
|
||||||
|
//! - [`DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) - the actual data,
|
||||||
|
//! with a specific type, has been received.
|
||||||
|
//! - [`TypedData`] - provides methods to read the actual data
|
||||||
|
//!
|
||||||
|
//! ### Sending a drag-and-drop operation
|
||||||
|
//!
|
||||||
|
//! - [`DataTransferSend`] - the core trait which defines data to be sent
|
||||||
|
//! - [`DataTransferSendBuilder`] - helper to create a new outgoing data transfer from a set of
|
||||||
|
//! types and callbacks that supply data of that type
|
||||||
|
//! - [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag) - the
|
||||||
|
//! application calls this to start a new drag operation
|
||||||
|
//! - [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped)/
|
||||||
|
//! [`OutgoingDragCanceled`](crate::event::WindowEvent::OutgoingDragCanceled) - the application
|
||||||
|
//! receives this when the user has ended the drag operation, by dropping the data or by canceling
|
||||||
|
//! the operation respectively
|
||||||
|
//!
|
||||||
|
//! # Detailed flow
|
||||||
|
//!
|
||||||
|
//! ## Receiving a drag-and-drop operation
|
||||||
|
//!
|
||||||
|
//! On all platforms, the process looks something like this:
|
||||||
|
//!
|
||||||
|
//! - A data transfer advertises a set of types which the data can be interpreted as. While the
|
||||||
|
//! precise implementation depends on platform, there's a set of types which can be safely
|
||||||
|
//! transferred between applications on all platforms (see [`TypeHint`]).
|
||||||
|
//! - For example, if you copy or drag text from a web page, the browser may advertise the text
|
||||||
|
//! formatted using HTML, the text formatted as RTF, and the text with all formatting removed
|
||||||
|
//! simultaneously.
|
||||||
|
//! - An application receiving a data transfer chooses one or more types that it understands and
|
||||||
|
//! requests the data in those formats (in practice, it will usually only request a single
|
||||||
|
//! format).
|
||||||
|
//! - The source application converts the data stored in its memory to the requested format and
|
||||||
|
//! asynchronously sends it to the target application
|
||||||
|
//!
|
||||||
|
//! On some platforms, the data is sometimes available synchronously, but all platforms have at
|
||||||
|
//! least some method of sending the data asynchronously and some types of data that may _only_ be
|
||||||
|
//! sent using the asynchronous interface. Because of this, the API in winit must be asynchronous.
|
||||||
|
//!
|
||||||
|
//! The flow for a user application that implements drag-and-drop would look something like this:
|
||||||
|
//!
|
||||||
|
//! - The application receives a [`DragEntered`](crate::event::WindowEvent::DragEntered) event. This
|
||||||
|
//! event supplies a [`DataTransferId`] which can be used to request information or operations on
|
||||||
|
//! the dragged data by using methods on [`Window`](crate::window::Window).
|
||||||
|
//! - To make sure that the operating system displays the correct cursor, and that modifier keys
|
||||||
|
//! will change the selected drag action correctly, the application should call
|
||||||
|
//! [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions). See
|
||||||
|
//! documentation on that method for details.
|
||||||
|
//! - As the drag operation continues, the window will receive
|
||||||
|
//! [`DragPosition`](crate::event::WindowEvent::DragPosition) events.
|
||||||
|
//! - At any point during this operation, the receiving application may request either the available
|
||||||
|
//! types or even the data being transferred. This may be useful in cases where the application
|
||||||
|
//! wants to preload the data. For example, an image editor may want to display the image on the
|
||||||
|
//! canvas during the drag operation.
|
||||||
|
//! - When the user tries to drop the data onto the window, that window will receive either a
|
||||||
|
//! [`DragDropped`](crate::event::WindowEvent::DragDropped) or
|
||||||
|
//! [`DragLeft`](crate::event::WindowEvent::DragLeft) event if the drag operation was accepted or
|
||||||
|
//! rejected, respectively. See documentation for
|
||||||
|
//! [`set_valid_dnd_actions`](crate::event_loop::ActiveEventLoop::set_valid_dnd_actions) for
|
||||||
|
//! details on accepting/rejecting a drag.
|
||||||
|
//!
|
||||||
|
//! ## Sending a drag-and-drop operation
|
||||||
|
//!
|
||||||
|
//! As the source application cannot interact with the ongoing drag while it is in-flight, this flow
|
||||||
|
//! is a lot simpler.
|
||||||
|
//!
|
||||||
|
//! - The application creates a [`DataTransferSend`] with a set of types and associated data. For
|
||||||
|
//! most cases, this can be done with [`DataTransferSendBuilder`].
|
||||||
|
//! - The application passes this [`DataTransferSend`] to
|
||||||
|
//! [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag)`. This is also
|
||||||
|
//! where metadata is set, such as the icon that will be shown during the drag operation.
|
||||||
|
//! - When the drag operation completes, the application receives
|
||||||
|
//! [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped) with the resultant
|
||||||
|
//! action, or [`OutgoingDragCanceled`](crate::event::WindowEvent::OutgoingDragCanceled), and
|
||||||
|
//! handles it appropriately. For example, if the drag was successful and the operation is
|
||||||
|
//! [`DndAction::Move`](crate::event_loop::DndAction::Move), then the application would delete the
|
||||||
|
//! source object, since the data has now been transferred somewhere else.
|
||||||
|
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
use std::ops::ControlFlow;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use crate::as_any::AsAny;
|
||||||
|
|
||||||
|
/// Unique identifier for a data transfer.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct DataTransferId(i64);
|
||||||
|
|
||||||
|
impl DataTransferId {
|
||||||
|
/// Convert the [`DataTransferId`] into the underlying integer.
|
||||||
|
///
|
||||||
|
/// This is useful if you need to pass the ID across an FFI boundary, or store it in an atomic.
|
||||||
|
pub const fn into_raw(self) -> i64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construct a [`DataTransferId`] from the underlying integer.
|
||||||
|
///
|
||||||
|
/// This should only be called with integers returned from [`DataTransferId::into_raw`].
|
||||||
|
pub const fn from_raw(id: i64) -> Self {
|
||||||
|
Self(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The set of types supported cross-platform.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum TypeHint {
|
||||||
|
/// Plain UTF-8 text (see [`TypedData::try_as_string`]).
|
||||||
|
///
|
||||||
|
/// **Note for platform implementations**: this hint is _only_ for UTF-8 text. If the platform
|
||||||
|
/// returns plaintext in some format other than UTF-8 by default, a [`TypedData`]
|
||||||
|
/// implementation marked with this type hint should convert to UTF-8.
|
||||||
|
Plaintext,
|
||||||
|
/// A list of URIs in the format defined by the `text/uri-list` MIME type, encoded as UTF-8 (see
|
||||||
|
/// [`TypedData::try_as_uris`]).
|
||||||
|
///
|
||||||
|
/// **Note for platform implementations**: this hint is _only_ for URIs encoded precisely in the
|
||||||
|
/// format specified above. If the platform uses a different format, a [`TypedData`]
|
||||||
|
/// implementation marked with this type hint should convert to that format.
|
||||||
|
UriList,
|
||||||
|
/// A HTML-formatted string
|
||||||
|
Html,
|
||||||
|
/// An RTF-formatted string
|
||||||
|
Rtf,
|
||||||
|
/// Audio
|
||||||
|
Audio {
|
||||||
|
/// An optional hint for the encoding of the supplied bytes, specified using the standard
|
||||||
|
/// file extension for that audio format, lowercase and without the leading `.`.
|
||||||
|
extension_hint: Option<&'static str>,
|
||||||
|
},
|
||||||
|
/// Image data
|
||||||
|
Image {
|
||||||
|
/// An optional hint for the encoding of the supplied bytes, specified using the standard
|
||||||
|
/// file extension for that image format, lowercase and without the leading `.`.
|
||||||
|
extension_hint: Option<&'static str>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeHint {
|
||||||
|
/// Check whether the two type hints "match".
|
||||||
|
///
|
||||||
|
/// This is subtly different to direct equality. If one of the types is an image or audio with a
|
||||||
|
/// `None` extension hint, then the other type just needs to match variant (i.e. image/audio),
|
||||||
|
/// the extension does not also have to be `None`.
|
||||||
|
pub fn matches(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::Plaintext, Self::Plaintext)
|
||||||
|
| (Self::UriList, Self::UriList)
|
||||||
|
| (Self::Html, Self::Html)
|
||||||
|
| (Self::Rtf, Self::Rtf) => true,
|
||||||
|
|
||||||
|
(
|
||||||
|
Self::Audio { extension_hint: this_ext },
|
||||||
|
Self::Audio { extension_hint: other_ext },
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
Self::Image { extension_hint: this_ext },
|
||||||
|
Self::Image { extension_hint: other_ext },
|
||||||
|
) => match (this_ext, other_ext) {
|
||||||
|
(Some(this_ext), Some(other_ext)) => this_ext == other_ext,
|
||||||
|
(None, _) | (_, None) => true,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type of a data transfer.
|
||||||
|
///
|
||||||
|
/// [`hint`](TransferType::hint) can be called to get the type in
|
||||||
|
/// a cross-platform format (see [`TypeHint`])
|
||||||
|
pub trait TransferType: AsAny + fmt::Debug {
|
||||||
|
/// Get the cross-platform representation of this type.
|
||||||
|
///
|
||||||
|
/// If this returns `None`, then this is a platform-dependent type that has no cross-platform
|
||||||
|
/// equivalent.
|
||||||
|
fn hint(&self) -> Option<TypeHint>;
|
||||||
|
|
||||||
|
/// Check whether two dynamically-typed transfer types are equivalent.
|
||||||
|
// Can't use a `PartialEq` bound because it causes a dependency cycle.
|
||||||
|
fn matches(&self, other: &dyn TransferType) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferType for TypeHint {
|
||||||
|
fn hint(&self) -> Option<TypeHint> {
|
||||||
|
Some(*self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, other: &dyn TransferType) -> bool {
|
||||||
|
other.hint().is_some_and(|hint| self.matches(&hint))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_dyn_casting!(TransferType);
|
||||||
|
|
||||||
|
// Replicates the cfg for `url::Url::parse`
|
||||||
|
#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit"))]
|
||||||
|
fn default_try_as_file_paths<T: TypedData + ?Sized>(data: &T) -> io::Result<Vec<PathBuf>> {
|
||||||
|
data.try_as_uris().and_then(|uris| {
|
||||||
|
uris.into_iter()
|
||||||
|
.map(|uri_string| {
|
||||||
|
Ok(url::Url::parse(&uri_string)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
|
||||||
|
.to_file_path()
|
||||||
|
.map_err(|()| io::ErrorKind::InvalidData)?)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replicates the cfg for `url::Url::parse`
|
||||||
|
//
|
||||||
|
// It doesn't matter that this is unimplemented on the web, as we don't currently support
|
||||||
|
// drag-and-drop for web targets and the web platform can't directly access paths anyway.
|
||||||
|
#[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit")))]
|
||||||
|
fn default_try_as_file_paths<T: TypedData + ?Sized>(_: &T) -> io::Result<Vec<PathBuf>> {
|
||||||
|
Err(io::ErrorKind::Unsupported.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data that has been fetched from a data transfer
|
||||||
|
///
|
||||||
|
/// ### Blocking
|
||||||
|
///
|
||||||
|
/// Note that this type provides a blocking interface. In cases where reading this type directly on
|
||||||
|
/// the event loop would cause a deadlock, the backend will make a best-effort attempt to return an
|
||||||
|
/// error with [`io::ErrorKind::Deadlock`]. For now, the only way to access the data is via blocking
|
||||||
|
/// on the event loop, so simply retrying the next time an event is received that references the
|
||||||
|
/// data transfer should be enough to ensure that the data is accessible.
|
||||||
|
pub trait TypedData: AsAny + fmt::Debug + Send + Sync {
|
||||||
|
/// The type of this `TypedData`.
|
||||||
|
fn type_(&self) -> &dyn TransferType;
|
||||||
|
|
||||||
|
/// If this value is readable as bytes, return a reader than can be used to read those bytes.
|
||||||
|
///
|
||||||
|
/// On some platforms, the reader must be driven incrementally upon each
|
||||||
|
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)`. If
|
||||||
|
/// you don't need to stream the data and just want the bytes in a single buffer, use
|
||||||
|
/// [`TypedData::try_as_bytes`].
|
||||||
|
fn try_read(&self) -> Option<Box<dyn io::BufRead>>;
|
||||||
|
|
||||||
|
/// If this value is readable as bytes, return those bytes.
|
||||||
|
///
|
||||||
|
/// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called
|
||||||
|
/// again upon next receiving
|
||||||
|
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)
|
||||||
|
fn try_as_bytes(&self) -> io::Result<Vec<u8>> {
|
||||||
|
let mut reader = self
|
||||||
|
.try_read()
|
||||||
|
.ok_or_else(|| io::Error::other("This `TypedData` is not readable as bytes"))?;
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
reader.read_to_end(&mut out)?;
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read this value as a list of URIs.
|
||||||
|
///
|
||||||
|
/// If this value is not readable as URIs, return an error.
|
||||||
|
///
|
||||||
|
/// The returned `String`s should be interpreted as URIs conforming to [RFC 3986](https://www.rfc-editor.org/info/rfc3986/).
|
||||||
|
///
|
||||||
|
/// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called
|
||||||
|
/// again upon next receiving
|
||||||
|
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)
|
||||||
|
fn try_as_uris(&self) -> io::Result<Vec<String>>;
|
||||||
|
|
||||||
|
/// Read this value as a list of paths.
|
||||||
|
///
|
||||||
|
/// This is provided as a convenience method to avoid the need for the user to manually parse
|
||||||
|
/// the result of [`try_as_uris`](TypedData::try_as_uris). `try_as_uris` should be preferred
|
||||||
|
/// when the extra complexity is acceptable, as it is more generic.
|
||||||
|
///
|
||||||
|
/// If this value is not readable as URIs, return an error.
|
||||||
|
///
|
||||||
|
/// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called
|
||||||
|
/// again upon next receiving
|
||||||
|
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)
|
||||||
|
fn try_as_file_paths(&self) -> io::Result<Vec<PathBuf>> {
|
||||||
|
default_try_as_file_paths(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read this value as a plain text string.
|
||||||
|
///
|
||||||
|
/// If this value is not readable as a string, return an error.
|
||||||
|
///
|
||||||
|
/// If this returns [`WouldBlock`](std::io::ErrorKind::WouldBlock), then it should be called
|
||||||
|
/// again upon next receiving
|
||||||
|
/// [`WindowEvent::DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived)
|
||||||
|
fn try_as_string(&self) -> io::Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required for `WindowEvent` to implement `PartialEq` - we just implement this on a best-effort
|
||||||
|
// basis.
|
||||||
|
impl PartialEq for dyn TypedData {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
std::ptr::addr_eq(self, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_dyn_casting!(TypedData);
|
||||||
|
|
||||||
|
/// Metadata about a data transfer. This does not allow actually receiving data, as that is an
|
||||||
|
/// asynchronous operation. To fetch the data from the source application, see
|
||||||
|
/// [`ActiveEventLoop::fetch_data_transfer`](crate::event_loop::ActiveEventLoop::fetch_data_transfer).
|
||||||
|
pub trait DataTransfer: AsAny + fmt::Debug {
|
||||||
|
/// Iterate over each type advertized by this `DataTransfer`. This is just a minor optimization,
|
||||||
|
/// in most cases you should probably use [`has_type`](DataTransfer::has_type) or
|
||||||
|
/// [`available_types`](DataTransfer::available_types).
|
||||||
|
fn for_each_available_type<'this>(
|
||||||
|
&'this self,
|
||||||
|
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Display the list of all available types.
|
||||||
|
///
|
||||||
|
/// This is useful if more-complex type matching is required, but for most cases
|
||||||
|
/// [`has_type`](DataTransfer::has_type) should be used.
|
||||||
|
fn available_types(&self) -> Vec<&'_ dyn TransferType> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
self.for_each_available_type(&mut |ty| {
|
||||||
|
out.push(ty);
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
});
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the supplied type is provided by this [`DataTransfer`].
|
||||||
|
///
|
||||||
|
/// Supplying a [`TypeHint`] as the type is supported on all platforms, but if some
|
||||||
|
/// platform-specific type is required then that platform's implementation of `TransferType` can
|
||||||
|
/// be used.
|
||||||
|
fn has_type(&self, type_: &dyn TransferType) -> bool {
|
||||||
|
let mut found = false;
|
||||||
|
self.for_each_available_type(&mut |haystack| {
|
||||||
|
if haystack.matches(type_) {
|
||||||
|
found = true;
|
||||||
|
ControlFlow::Break(())
|
||||||
|
} else {
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_dyn_casting!(DataTransfer);
|
||||||
|
|
||||||
|
/// Kinds of data that can be sent via a `DataTransfer`.
|
||||||
|
///
|
||||||
|
/// Some kinds of data cannot be represented by just a binary blob in a cross-platform way.
|
||||||
|
/// File URIs on Windows and macOS are represented as arrays of strings, and strings have
|
||||||
|
/// different encoding on different platforms. To allow this to be represented, we allow
|
||||||
|
/// supplying strings and URIs separately from binary blobs.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum SendData {
|
||||||
|
/// List of URIs.
|
||||||
|
///
|
||||||
|
/// These should conform to [RFC 3986](https://www.rfc-editor.org/info/rfc3986/).
|
||||||
|
/// If you just want to send file paths, see [`SendData::from_file_paths`].
|
||||||
|
///
|
||||||
|
/// Note that `SendData` implements `From<String>` and `From<Vec<u8>>`, but _not_
|
||||||
|
/// `From<Vec<String>>`, as it is not necessarily obvious to a reader that `Vec<String>`
|
||||||
|
/// will be interpreted as a URI list. However, it _does_ implement [`From<Url>`](url::Url),
|
||||||
|
/// if you are using the [`url`](https://docs.rs/url/2) crate.
|
||||||
|
Uris(Vec<String>),
|
||||||
|
/// String
|
||||||
|
///
|
||||||
|
/// This can also be constructed with the [`From<String>`](std::string::String) implementation.
|
||||||
|
String(String),
|
||||||
|
/// Binary blob
|
||||||
|
///
|
||||||
|
/// This can also be constructed with the [`From<Vec<u8>>`](std::vec::Vec) implementation.
|
||||||
|
Bytes(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SendData {
|
||||||
|
/// Create [`SendData::Uris`] from an iterator of [`Path`]s.
|
||||||
|
///
|
||||||
|
/// All paths must be absolute, and on Windows must include either a drive prefix (e.g. `C:\`)
|
||||||
|
/// or a UNC prefix (`\\`). See documentation for [`url::Url::from_file_path`].
|
||||||
|
pub fn from_file_paths<I>(paths: I) -> Option<Self>
|
||||||
|
where
|
||||||
|
I: IntoIterator,
|
||||||
|
I::Item: AsRef<Path>,
|
||||||
|
{
|
||||||
|
// Replicates the cfg for `url::Url::from_file_path`
|
||||||
|
#[cfg(any(unix, windows, target_os = "redox", target_os = "wasi", target_os = "hermit"))]
|
||||||
|
fn from_file_paths_impl<I>(paths: I) -> Option<SendData>
|
||||||
|
where
|
||||||
|
I: IntoIterator,
|
||||||
|
I::Item: AsRef<Path>,
|
||||||
|
{
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(url::Url::from_file_path)
|
||||||
|
.map(|result| result.map(String::from))
|
||||||
|
.collect::<Result<Vec<_>, ()>>()
|
||||||
|
.map(SendData::Uris)
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replicates the cfg for `url::Url::from_file_path`
|
||||||
|
//
|
||||||
|
// It doesn't matter that this is unimplemented on the web, as we don't currently support
|
||||||
|
// drag-and-drop for web targets and the web platform can't directly access paths
|
||||||
|
// anyway.
|
||||||
|
#[cfg(not(any(
|
||||||
|
unix,
|
||||||
|
windows,
|
||||||
|
target_os = "redox",
|
||||||
|
target_os = "wasi",
|
||||||
|
target_os = "hermit"
|
||||||
|
)))]
|
||||||
|
fn from_file_paths_impl<I>(_: I) -> Option<SendData> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
from_file_paths_impl(paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We monomorphize these `From` implementations instead of making them generic, in order to
|
||||||
|
// prevent accidentally casting to the wrong type.
|
||||||
|
impl From<String> for SendData {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self::String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<u8>> for SendData {
|
||||||
|
fn from(value: Vec<u8>) -> Self {
|
||||||
|
Self::Bytes(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<url::Url>> for SendData {
|
||||||
|
fn from(value: Vec<url::Url>) -> Self {
|
||||||
|
Self::Uris(value.into_iter().map(Into::into).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for sending data via a data transfer.
|
||||||
|
///
|
||||||
|
/// See [`ActiveEventLoop::start_drag`](crate::event_loop::ActiveEventLoop::start_drag) for where
|
||||||
|
/// this is used. To build an implementation of this trait dynamically in a cross-platform way, use
|
||||||
|
/// [`DataTransferSendBuilder`].
|
||||||
|
pub trait DataTransferSend: DataTransfer + Send {
|
||||||
|
/// Get the data for the specified type, or `None` if this value does not supply the given data
|
||||||
|
/// type.
|
||||||
|
fn data_for_type(&self, type_: &dyn TransferType) -> Option<SendData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_dyn_casting!(DataTransferSend);
|
||||||
|
|
||||||
|
type SendDataCallback<T> = Box<dyn Fn(&T, &dyn TransferType) -> Option<SendData> + Send>;
|
||||||
|
|
||||||
|
/// Dynamic builder for an implementation of [`DataTransferSend`].
|
||||||
|
///
|
||||||
|
/// On all platforms, inter-application data transfer (i.e. clipboard and drag-and-drop) works like
|
||||||
|
/// so:
|
||||||
|
///
|
||||||
|
/// - The source advertises a set of types that it can transfer.
|
||||||
|
/// - The destination picks one or more of those types to receive.
|
||||||
|
/// - The source sends the data for that type.
|
||||||
|
///
|
||||||
|
/// This type abstracts that in a way that allows data to be sent cross-platform. `T` is an optional
|
||||||
|
/// state value, which allows the user to have a single source of truth for their data, converting
|
||||||
|
/// it lazily to the requested type.
|
||||||
|
pub struct DataTransferSendBuilder<T> {
|
||||||
|
state: T,
|
||||||
|
types: Vec<(Box<dyn TransferType + Send>, SendDataCallback<T>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> fmt::Debug for DataTransferSendBuilder<T>
|
||||||
|
where
|
||||||
|
T: fmt::Debug,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("NewDataTransferBuilder").field("state", &self.state).finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DataTransfer for DataTransferSendBuilder<T>
|
||||||
|
where
|
||||||
|
T: fmt::Debug + Send + 'static,
|
||||||
|
{
|
||||||
|
fn for_each_available_type<'this>(
|
||||||
|
&'this self,
|
||||||
|
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> ControlFlow<()>,
|
||||||
|
) {
|
||||||
|
let _ = self.types.iter().try_for_each(|(ty, _)| func(&**ty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DataTransferSend for DataTransferSendBuilder<T>
|
||||||
|
where
|
||||||
|
T: fmt::Debug + Send + 'static,
|
||||||
|
{
|
||||||
|
fn data_for_type(&self, type_: &dyn TransferType) -> Option<SendData> {
|
||||||
|
self.data_for_type(type_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DataTransferSendBuilder<T> {
|
||||||
|
/// Create a new [`DataTransferSendBuilder`], with a state value which acts as
|
||||||
|
/// the single source of truth for the underlying data.
|
||||||
|
pub fn new(state: T) -> Self {
|
||||||
|
Self { state, types: vec![] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DataTransferSendBuilder<T> {
|
||||||
|
fn data_for_type(&self, type_: &dyn TransferType) -> Option<SendData> {
|
||||||
|
let (_, func) = self.types.iter().find(|(ty, _)| ty.matches(type_))?;
|
||||||
|
|
||||||
|
func(&self.state, type_)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a callback which converts the builder's state to the given type. In
|
||||||
|
/// most cases, `type_` will be [`TypeHint`].
|
||||||
|
pub fn add_type<Ty, F, O>(&mut self, type_: Ty, func: F) -> &mut Self
|
||||||
|
where
|
||||||
|
Ty: TransferType + Send,
|
||||||
|
F: Fn(&T, &dyn TransferType) -> Option<O> + Send + 'static,
|
||||||
|
O: Into<SendData>,
|
||||||
|
{
|
||||||
|
self.types
|
||||||
|
.push((Box::new(type_), Box::new(move |state, ty| func(state, ty).map(Into::into))));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a new builder, adding a callback which converts the builder's state
|
||||||
|
/// to the given type.
|
||||||
|
///
|
||||||
|
/// For cross-platform use, `type_` will be [`TypeHint`]. The closure additionally receives
|
||||||
|
/// a [`TransferType`], which is not necessarily the same as `type_` for the following reasons:
|
||||||
|
///
|
||||||
|
/// - The OS may have multiple types which are equivalent to the supplied type
|
||||||
|
/// - `TypeHint::Audio` and `TypeHint::Image` with `extension_hint: None` will advertise all
|
||||||
|
/// supported audio and image formats, in which case the closure may receive a type with an
|
||||||
|
/// extension chosen by the receiving application.
|
||||||
|
pub fn with_type<Ty, F, O>(mut self, type_: Ty, func: F) -> Self
|
||||||
|
where
|
||||||
|
Ty: TransferType + Send,
|
||||||
|
F: Fn(&T, &dyn TransferType) -> Option<O> + Send + 'static,
|
||||||
|
O: Into<SendData>,
|
||||||
|
{
|
||||||
|
self.add_type(type_, func);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DataTransferSendBuilder<T>
|
||||||
|
where
|
||||||
|
T: fmt::Debug + Send + 'static,
|
||||||
|
{
|
||||||
|
/// Consume the builder, returning an implementation of [`DataTransferSend`].
|
||||||
|
///
|
||||||
|
/// Note that this is only provided for explicitness and ergonomics. [`DataTransferSendBuilder`]
|
||||||
|
/// implements [`DataTransferSend`] and this method is equivalent to [`Box::new`].
|
||||||
|
pub fn build(self) -> Box<dyn DataTransferSend> {
|
||||||
|
Box::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
use std::cell::LazyCell;
|
use std::cell::LazyCell;
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::f64;
|
use std::f64;
|
||||||
use std::path::PathBuf;
|
use std::sync::{Arc, Mutex, Weak};
|
||||||
use std::sync::{Mutex, Weak};
|
|
||||||
|
|
||||||
use dpi::{PhysicalPosition, PhysicalSize};
|
use dpi::{PhysicalPosition, PhysicalSize};
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
@@ -11,8 +10,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
use smol_str::SmolStr;
|
use smol_str::SmolStr;
|
||||||
|
|
||||||
use crate::Instant;
|
use crate::Instant;
|
||||||
|
use crate::data_transfer::{DataTransferId, TypedData};
|
||||||
use crate::error::RequestError;
|
use crate::error::RequestError;
|
||||||
use crate::event_loop::AsyncRequestSerial;
|
use crate::event_loop::{AsyncRequestSerial, DndAction};
|
||||||
use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState};
|
use crate::keyboard::{self, ModifiersKeyState, ModifiersKeys, ModifiersState};
|
||||||
#[cfg(doc)]
|
#[cfg(doc)]
|
||||||
use crate::window::Window;
|
use crate::window::Window;
|
||||||
@@ -75,42 +75,104 @@ pub enum WindowEvent {
|
|||||||
/// The window has been destroyed.
|
/// The window has been destroyed.
|
||||||
Destroyed,
|
Destroyed,
|
||||||
|
|
||||||
/// A file drag operation has entered the window.
|
/// A drag operation has entered the window.
|
||||||
|
///
|
||||||
|
/// The user can use the `id` to read information about the incoming dragged data, and report
|
||||||
|
/// whether the operation is accepted or rejected back to the operating system (see
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`](`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`)).
|
||||||
|
///
|
||||||
|
/// To read the data being dragged, see
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::fetch_data_transfer`](`crate::event_loop::ActiveEventLoop::fetch_data_transfer`).
|
||||||
DragEntered {
|
DragEntered {
|
||||||
/// List of paths that are being dragged onto the window.
|
/// ID of the data transfer object, see
|
||||||
paths: Vec<PathBuf>,
|
/// [`crate::event_loop::ActiveEventLoop::data_transfer`](`crate::event_loop::ActiveEventLoop::data_transfer`).
|
||||||
/// (x,y) coordinates in pixels relative to the top-left corner of the window. May be
|
id: DataTransferId,
|
||||||
/// negative on some platforms if something is dragged over a window's decorations (title
|
/// (x,y) coordinates in pixels relative to the top-left corner of the window.
|
||||||
/// bar, frame, etc).
|
|
||||||
position: PhysicalPosition<f64>,
|
|
||||||
},
|
|
||||||
/// A file drag operation has moved over the window.
|
|
||||||
DragMoved {
|
|
||||||
/// (x,y) coordinates in pixels relative to the top-left corner of the window. May be
|
|
||||||
/// negative on some platforms if something is dragged over a window's decorations (title
|
|
||||||
/// bar, frame, etc).
|
|
||||||
position: PhysicalPosition<f64>,
|
|
||||||
},
|
|
||||||
/// The file drag operation has dropped file(s) on the window.
|
|
||||||
DragDropped {
|
|
||||||
/// List of paths that are being dragged onto the window.
|
|
||||||
paths: Vec<PathBuf>,
|
|
||||||
/// (x,y) coordinates in pixels relative to the top-left corner of the window. May be
|
|
||||||
/// negative on some platforms if something is dragged over a window's decorations (title
|
|
||||||
/// bar, frame, etc).
|
|
||||||
position: PhysicalPosition<f64>,
|
|
||||||
},
|
|
||||||
/// The file drag operation has been cancelled or left the window.
|
|
||||||
DragLeft {
|
|
||||||
/// (x,y) coordinates in pixels relative to the top-left corner of the window. May be
|
|
||||||
/// negative on some platforms if something is dragged over a window's decorations (title
|
|
||||||
/// bar, frame, etc).
|
|
||||||
///
|
///
|
||||||
/// ## Platform-specific
|
/// May be negative on some platforms if something is dragged over a window's decorations
|
||||||
|
/// (title bar, frame, etc).
|
||||||
///
|
///
|
||||||
/// - **Windows:** Always emits [`None`].
|
/// Some platforms will provide this on enter, others do not. If
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`](`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`)
|
||||||
|
/// is never called, the default state is for the drag operation to be rejected. The
|
||||||
|
/// position is provided here when available to allow the application to accept a drag
|
||||||
|
/// operation as soon as possible, preventing the cursor from flickering from rejected to
|
||||||
|
/// accepted.
|
||||||
position: Option<PhysicalPosition<f64>>,
|
position: Option<PhysicalPosition<f64>>,
|
||||||
},
|
},
|
||||||
|
/// The position of an ongoing drag operation has changed.
|
||||||
|
DragPosition {
|
||||||
|
/// ID of the data transfer object, see
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::data_transfer`](`crate::event_loop::ActiveEventLoop::data_transfer`).
|
||||||
|
id: DataTransferId,
|
||||||
|
/// (x,y) coordinates in pixels relative to the top-left corner of the window.
|
||||||
|
///
|
||||||
|
/// May be negative on some platforms if something is dragged over a window's decorations
|
||||||
|
/// (title bar, frame, etc).
|
||||||
|
position: PhysicalPosition<f64>,
|
||||||
|
/// The drag action proposed by the OS, based on the actions supplied in
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`], the actions available on
|
||||||
|
/// the source, and the held modifier keys.
|
||||||
|
///
|
||||||
|
/// This may be `None` if the backend has not supplied a valid action. On some platforms
|
||||||
|
/// (in particular, X11), the application is only informed of the proposed action once
|
||||||
|
/// the operation completes.
|
||||||
|
proposed_action: Option<DndAction>,
|
||||||
|
},
|
||||||
|
/// A drag operation has dropped file(s) on the window.
|
||||||
|
DragDropped {
|
||||||
|
/// ID of the data transfer object, see
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::data_transfer`].
|
||||||
|
id: DataTransferId,
|
||||||
|
/// The drag action proposed by the OS, based on the actions supplied in
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::set_valid_dnd_actions`], the actions available on
|
||||||
|
/// the source, and the held modifier keys.
|
||||||
|
///
|
||||||
|
/// This may be `None` if the backend has not supplied a valid action. This is different
|
||||||
|
/// from the drag being canceled: the drag completed successfully, we just don't know
|
||||||
|
/// what action was selected.
|
||||||
|
proposed_action: Option<DndAction>,
|
||||||
|
},
|
||||||
|
/// A drag operation has been canceled or left the window.
|
||||||
|
DragLeft {
|
||||||
|
/// ID of the data transfer object, see
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::data_transfer`].
|
||||||
|
id: DataTransferId,
|
||||||
|
},
|
||||||
|
/// Data is available for a specific fetch request, see
|
||||||
|
/// [`fetch_data_transfer`](crate::event_loop::ActiveEventLoop::data_transfer).
|
||||||
|
///
|
||||||
|
/// While winit makes a best effort to only send this event precisely once, on some platforms it
|
||||||
|
/// may not be possible to uniquely determine the window that should receive it. In these
|
||||||
|
/// cases, winit may dispatch the event to all windows that have access to the data
|
||||||
|
/// transfer. If your application should only process this event once per data transfer, the
|
||||||
|
/// `serial` field can be used to deduplicate it.
|
||||||
|
DataTransferReceived {
|
||||||
|
/// ID of the data transfer object, see
|
||||||
|
/// [`crate::event_loop::ActiveEventLoop::data_transfer`].
|
||||||
|
id: DataTransferId,
|
||||||
|
/// Serial returned from `fetch_data_transfer`.
|
||||||
|
serial: AsyncRequestSerial,
|
||||||
|
/// The data for the transfer, with a specific type.
|
||||||
|
value: Arc<dyn TypedData>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// A drag operation started with `start_drag` has been dropped.
|
||||||
|
OutgoingDragDropped {
|
||||||
|
/// The ID returned from `start_drag`
|
||||||
|
id: DataTransferId,
|
||||||
|
/// The operation selected by the drop destination.
|
||||||
|
///
|
||||||
|
/// This may be `None` if the backend has not supplied a valid action. This is different
|
||||||
|
/// from the drag being canceled: the drag completed successfully, we just don't know
|
||||||
|
/// what action was selected.
|
||||||
|
action: Option<DndAction>,
|
||||||
|
},
|
||||||
|
/// A drag operation started with `start_drag` has been canceled.
|
||||||
|
OutgoingDragCanceled {
|
||||||
|
/// The ID returned from `start_drag`
|
||||||
|
id: DataTransferId,
|
||||||
|
},
|
||||||
|
|
||||||
/// The window gained or lost focus.
|
/// The window gained or lost focus.
|
||||||
///
|
///
|
||||||
@@ -1582,16 +1644,20 @@ mod tests {
|
|||||||
use crate::event::Ime::Enabled;
|
use crate::event::Ime::Enabled;
|
||||||
use crate::event::WindowEvent::*;
|
use crate::event::WindowEvent::*;
|
||||||
use crate::event::{PointerKind, PointerSource};
|
use crate::event::{PointerKind, PointerSource};
|
||||||
|
use crate::event_loop::DndAction;
|
||||||
|
use crate::data_transfer::DataTransferId;
|
||||||
|
|
||||||
|
let dnd_data = DataTransferId::from_raw(123);
|
||||||
|
|
||||||
with_window_event(CloseRequested);
|
with_window_event(CloseRequested);
|
||||||
with_window_event(Destroyed);
|
with_window_event(Destroyed);
|
||||||
with_window_event(Focused(true));
|
with_window_event(Focused(true));
|
||||||
with_window_event(Moved((0, 0).into()));
|
with_window_event(Moved((0, 0).into()));
|
||||||
with_window_event(SurfaceResized((0, 0).into()));
|
with_window_event(SurfaceResized((0, 0).into()));
|
||||||
with_window_event(DragEntered { paths: vec!["x.txt".into()], position: (0, 0).into() });
|
with_window_event(DragEntered { id: dnd_data, position: None });
|
||||||
with_window_event(DragMoved { position: (0, 0).into() });
|
with_window_event(DragPosition { id: dnd_data, position: (0, 0).into(), proposed_action: Some(DndAction::Copy) });
|
||||||
with_window_event(DragDropped { paths: vec!["x.txt".into()], position: (0, 0).into() });
|
with_window_event(DragDropped { id: dnd_data, proposed_action: Some(DndAction::Copy) });
|
||||||
with_window_event(DragLeft { position: Some((0, 0).into()) });
|
with_window_event(DragLeft { id: dnd_data });
|
||||||
with_window_event(Ime(Enabled));
|
with_window_event(Ime(Enabled));
|
||||||
with_window_event(PointerMoved {
|
with_window_event(PointerMoved {
|
||||||
device_id: None,
|
device_id: None,
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ use rwh_06::{DisplayHandle, HandleError, HasDisplayHandle};
|
|||||||
use crate::Instant;
|
use crate::Instant;
|
||||||
use crate::as_any::AsAny;
|
use crate::as_any::AsAny;
|
||||||
use crate::cursor::{CustomCursor, CustomCursorSource};
|
use crate::cursor::{CustomCursor, CustomCursorSource};
|
||||||
use crate::error::RequestError;
|
use crate::data_transfer::{DataTransfer, DataTransferId, DataTransferSend, TransferType};
|
||||||
|
use crate::error::{NotSupportedError, RequestError};
|
||||||
|
use crate::icon::Icon;
|
||||||
use crate::monitor::MonitorHandle;
|
use crate::monitor::MonitorHandle;
|
||||||
use crate::window::{Theme, Window, WindowAttributes};
|
use crate::window::{Theme, Window, WindowAttributes, WindowId};
|
||||||
|
|
||||||
pub trait ActiveEventLoop: AsAny + fmt::Debug {
|
pub trait ActiveEventLoop: AsAny + fmt::Debug {
|
||||||
/// Creates an [`EventLoopProxy`] that can be used to dispatch user events
|
/// Creates an [`EventLoopProxy`] that can be used to dispatch user events
|
||||||
@@ -114,8 +116,136 @@ pub trait ActiveEventLoop: AsAny + fmt::Debug {
|
|||||||
|
|
||||||
/// Get the raw-window-handle handle.
|
/// Get the raw-window-handle handle.
|
||||||
fn rwh_06_handle(&self) -> &dyn HasDisplayHandle;
|
fn rwh_06_handle(&self) -> &dyn HasDisplayHandle;
|
||||||
|
|
||||||
|
/// Request to fetch a type from a [data transfer](crate::data_transfer::DataTransfer).
|
||||||
|
///
|
||||||
|
/// This may be called multiple times on the same [`DataTransferId`] with different types,
|
||||||
|
/// and may be called at any point during the drag operation, including during handling the
|
||||||
|
/// [`DragDropped`](crate::event::WindowEvent::DragDropped) event. After that event has been
|
||||||
|
/// received, though, the data transfer is not guaranteed to be available. The data is
|
||||||
|
/// _not_ guaranteed to be available during (or after) handling of
|
||||||
|
/// [`DragLeft](crate::event::WindowEvent::DragLeft).
|
||||||
|
///
|
||||||
|
/// Once available, the data will be supplied to the application with the
|
||||||
|
/// [`DataTransferReceived`](crate::event::WindowEvent::DataTransferReceived) event.
|
||||||
|
fn fetch_data_transfer(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
type_: &dyn TransferType,
|
||||||
|
) -> Result<AsyncRequestSerial, RequestError> {
|
||||||
|
let _ = id;
|
||||||
|
let _ = type_;
|
||||||
|
Err(RequestError::NotSupported(NotSupportedError::new(
|
||||||
|
DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a [data transfer](DataTransfer) by its ID.
|
||||||
|
///
|
||||||
|
/// If the ID is invalid (e.g. if the lifetime of the data transfer has expired), this will
|
||||||
|
/// return an error.
|
||||||
|
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
|
||||||
|
let _ = id;
|
||||||
|
Err(RequestError::NotSupported(NotSupportedError::new(
|
||||||
|
DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a given set of `DndAction`s as the valid actions for the given [`DataTransferId`],
|
||||||
|
/// if the transfer ID is from an incoming drag-and-drop operation.
|
||||||
|
///
|
||||||
|
/// This allows the OS/compositor to display the correct UI, indicating that the dragged data
|
||||||
|
/// can be dropped. If the data transfer does not exist or is not from a drag-and-drop
|
||||||
|
/// operation, will return an error.
|
||||||
|
///
|
||||||
|
/// The operating system will consider the drag either accepted or rejected based on the
|
||||||
|
/// set of valid actions supplied using this method, combined with the set of valid actions
|
||||||
|
/// on the drag source. If the drag is rejected at the point that the user finalizes the drop,
|
||||||
|
/// the application will receive [`DragLeft`](crate::event::WindowEvent::DragLeft) instead
|
||||||
|
/// of [`DragDropped`](crate::event::WindowEvent::DragDropped).
|
||||||
|
///
|
||||||
|
/// Note that _rejecting_ the drag is not the same as _canceling_ the drag. A rejected drag can
|
||||||
|
/// be accepted later and the user can continue dragging it over other potential targets. On
|
||||||
|
/// most platforms, there is no way for an application to explicitly cancel a drag
|
||||||
|
/// operation.
|
||||||
|
///
|
||||||
|
/// The set of actions is expected to be ordered by preference.
|
||||||
|
fn set_valid_dnd_actions(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
actions: &[DndAction],
|
||||||
|
) -> Result<(), RequestError> {
|
||||||
|
let _ = id;
|
||||||
|
let _ = actions;
|
||||||
|
Err(RequestError::NotSupported(NotSupportedError::new(
|
||||||
|
DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initiate a new drag-and-drop operation.
|
||||||
|
///
|
||||||
|
/// See [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSendBuilder) for how to
|
||||||
|
/// create a new cross-platform data transfer, or [`DataTransferSend`] for a generic trait
|
||||||
|
/// which can be implemented manually.
|
||||||
|
///
|
||||||
|
/// The [`DataTransferId`] returned from this method, identifying the outgoing drag, is
|
||||||
|
/// currently only used for identifying the drag in the
|
||||||
|
/// [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped) event. In most
|
||||||
|
/// cases, a drag will be started while the mouse is over the window which started it. This
|
||||||
|
/// means that, directly after this method is called, the window will then receive a
|
||||||
|
/// [`DragEntered`](crate::event::WindowEvent::DragEntered) event. However, the ID identifying
|
||||||
|
/// the incoming drag is not guaranteed to be the same as the ID returned from this method.
|
||||||
|
///
|
||||||
|
/// For most cases, applications can treat all `DragEntered` events the same, whether they were
|
||||||
|
/// initiated by the same application or a different application. However, if the user wants to
|
||||||
|
/// have some kind of special handling for internal drag-and-drop, they will currently need
|
||||||
|
/// to implement it via workaround. On all systems where drag-and-drop is implemented in
|
||||||
|
/// Winit, the application can make the assumption that only a single drag operation can
|
||||||
|
/// occur at one time. Therefore, if `DragEntered` is received between calling this method
|
||||||
|
/// and receiving `OutgoingDragDropped`, then you can assume that it's the same drag.
|
||||||
|
/// In theory, Wayland allows multiple simultaneous drag operations at a time, but Winit does
|
||||||
|
/// not currently guarantee that this is supported correctly for either internal or external
|
||||||
|
/// drag.
|
||||||
|
///
|
||||||
|
/// ### Arguments
|
||||||
|
///
|
||||||
|
/// - `source` - The ID of the window that initiated the drag operation.
|
||||||
|
/// - `send_data` - The data provided by this drag operation. See
|
||||||
|
/// [`DataTransferSendBuilder`](crate::data_transfer::DataTransferSendBuilder).
|
||||||
|
/// - `actions` - The set of valid actions for this drag operation. See [`DndAction`]. On
|
||||||
|
/// Wayland, this is expected to be ordered by preference.
|
||||||
|
/// - `icon` - The icon to show while dragging.
|
||||||
|
///
|
||||||
|
/// Some platforms have a more-expressive way of setting the visual component of a drag
|
||||||
|
/// operation. For those platforms, consider using the platform-specific implementation of
|
||||||
|
/// [`DataTransferSend`] for `send_data` and set this field to `None`.
|
||||||
|
///
|
||||||
|
/// ### Returns
|
||||||
|
///
|
||||||
|
/// A unique identifier for this drag operation, which will be later supplied by
|
||||||
|
/// [`OutgoingDragDropped`](crate::event::WindowEvent::OutgoingDragDropped).
|
||||||
|
fn start_drag(
|
||||||
|
&self,
|
||||||
|
source: WindowId,
|
||||||
|
send_data: Box<dyn DataTransferSend>,
|
||||||
|
actions: &[DndAction],
|
||||||
|
icon: Option<DragIcon>,
|
||||||
|
) -> Result<DataTransferId, RequestError> {
|
||||||
|
let _ = source;
|
||||||
|
let _ = send_data;
|
||||||
|
let _ = actions;
|
||||||
|
let _ = icon;
|
||||||
|
Err(RequestError::NotSupported(NotSupportedError::new(
|
||||||
|
DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATA_TRANSFER_UNSUPPORTED_ERROR_MESSAGE: &str = {
|
||||||
|
"Cross-application data transfer (e.g. drag-and-drop, clipboard) is unsupported on this \
|
||||||
|
platform"
|
||||||
|
};
|
||||||
|
|
||||||
impl HasDisplayHandle for dyn ActiveEventLoop + '_ {
|
impl HasDisplayHandle for dyn ActiveEventLoop + '_ {
|
||||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||||
self.rwh_06_handle().display_handle()
|
self.rwh_06_handle().display_handle()
|
||||||
@@ -124,6 +254,73 @@ impl HasDisplayHandle for dyn ActiveEventLoop + '_ {
|
|||||||
|
|
||||||
impl_dyn_casting!(ActiveEventLoop);
|
impl_dyn_casting!(ActiveEventLoop);
|
||||||
|
|
||||||
|
/// Information needed to initiate a new drag operation.
|
||||||
|
pub struct DragIcon {
|
||||||
|
/// The icon to apply to the cursor.
|
||||||
|
pub icon: Icon,
|
||||||
|
/// An x offset applied to the dragged icon.
|
||||||
|
///
|
||||||
|
/// This is specified in image pixels. 0 means that the left side of the icon will be at
|
||||||
|
/// the cursor.
|
||||||
|
pub offset_x: i32,
|
||||||
|
/// A y offset applied to the dragged icon.
|
||||||
|
///
|
||||||
|
/// This is specified in image pixels. 0 means that the top of the icon will be at the
|
||||||
|
/// cursor.
|
||||||
|
pub offset_y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Icon> for DragIcon {
|
||||||
|
fn from(value: Icon) -> Self {
|
||||||
|
Self { icon: value, offset_x: 0, offset_y: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The set of available actions for a drag operation.
|
||||||
|
///
|
||||||
|
/// This is _not_ a bitset, as on some platforms (e.g. Wayland, macOS) the source and/or destination
|
||||||
|
/// are expected to provide some kind of order of preference.
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum DndAction {
|
||||||
|
/// Move the dragged item from the source to the destination.
|
||||||
|
///
|
||||||
|
/// # Platforms
|
||||||
|
///
|
||||||
|
/// - Wayland
|
||||||
|
/// - macOS
|
||||||
|
/// - Windows
|
||||||
|
Move,
|
||||||
|
/// Copy the dragged item from the source to the destination.
|
||||||
|
///
|
||||||
|
/// # Platforms
|
||||||
|
///
|
||||||
|
/// - X11
|
||||||
|
/// - Wayland
|
||||||
|
/// - macOS
|
||||||
|
/// - Windows
|
||||||
|
Copy,
|
||||||
|
/// A link is established between the source and the destination.
|
||||||
|
///
|
||||||
|
/// # Platforms
|
||||||
|
///
|
||||||
|
/// - macOS
|
||||||
|
/// - Windows
|
||||||
|
Link,
|
||||||
|
/// The user will be prompted for what should be done
|
||||||
|
///
|
||||||
|
/// # Platforms
|
||||||
|
///
|
||||||
|
/// - Wayland
|
||||||
|
Ask,
|
||||||
|
/// The source and destination will negotiate the drag operation privately
|
||||||
|
///
|
||||||
|
/// # Platforms
|
||||||
|
///
|
||||||
|
/// - macOS
|
||||||
|
Private,
|
||||||
|
}
|
||||||
|
|
||||||
/// Control the [`ActiveEventLoop`], possibly from a different thread, without referencing it
|
/// Control the [`ActiveEventLoop`], possibly from a different thread, without referencing it
|
||||||
/// directly.
|
/// directly.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub mod cursor;
|
|||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod application;
|
pub mod application;
|
||||||
|
pub mod data_transfer;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod event_loop;
|
pub mod event_loop;
|
||||||
pub mod icon;
|
pub mod icon;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ serde = ["dep:serde", "bitflags/serde", "smol_str/serde", "dpi/serde"]
|
|||||||
bitflags.workspace = true
|
bitflags.workspace = true
|
||||||
cursor-icon.workspace = true
|
cursor-icon.workspace = true
|
||||||
dpi.workspace = true
|
dpi.workspace = true
|
||||||
|
percent-encoding.workspace = true
|
||||||
rwh_06.workspace = true
|
rwh_06.workspace = true
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
smol_str.workspace = true
|
smol_str.workspace = true
|
||||||
@@ -40,7 +41,7 @@ sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-feature
|
|||||||
sctk-adwaita = { version = "0.11.0", default-features = false, optional = true }
|
sctk-adwaita = { version = "0.11.0", default-features = false, optional = true }
|
||||||
wayland-backend = { version = "0.3.10", default-features = false, features = ["client_system"] }
|
wayland-backend = { version = "0.3.10", default-features = false, features = ["client_system"] }
|
||||||
wayland-client = "0.31.10"
|
wayland-client = "0.31.10"
|
||||||
wayland-protocols = { version = "0.32.11", features = ["staging", "unstable"] }
|
wayland-protocols = { version = "0.32.12", features = ["staging", "unstable"] }
|
||||||
wayland-protocols-plasma = { version = "0.3.8", features = ["client"] }
|
wayland-protocols-plasma = { version = "0.3.8", features = ["client"] }
|
||||||
winit-common = { workspace = true, features = ["xkb", "wayland"] }
|
winit-common = { workspace = true, features = ["xkb", "wayland"] }
|
||||||
|
|
||||||
|
|||||||
805
winit-wayland/src/dnd.rs
Normal file
805
winit-wayland/src/dnd.rs
Normal file
@@ -0,0 +1,805 @@
|
|||||||
|
//! Types related to drag-and-drop and data transfer on Wayland.
|
||||||
|
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::{self, BufRead, Cursor, ErrorKind, Write};
|
||||||
|
use std::ops::{BitOr, Deref};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use calloop::PostAction;
|
||||||
|
use dpi::{LogicalPosition, PhysicalPosition};
|
||||||
|
use sctk::data_device_manager::WritePipe;
|
||||||
|
use sctk::data_device_manager::data_device::{DataDeviceData, DataDeviceHandler};
|
||||||
|
use sctk::data_device_manager::data_offer::{DataOfferHandler, DragOffer};
|
||||||
|
use sctk::data_device_manager::data_source::{DataSourceHandler, DragSource as SctkDragSource};
|
||||||
|
use sctk::reexports::client::backend::ObjectId;
|
||||||
|
use wayland_client::protocol::wl_data_device::WlDataDevice;
|
||||||
|
use wayland_client::protocol::wl_data_device_manager::DndAction as WlDndAction;
|
||||||
|
use wayland_client::protocol::wl_data_offer::WlDataOffer;
|
||||||
|
use wayland_client::protocol::wl_data_source::WlDataSource;
|
||||||
|
use wayland_client::protocol::wl_surface::WlSurface;
|
||||||
|
use wayland_client::{Connection, Proxy, QueueHandle};
|
||||||
|
use winit_core::data_transfer::{
|
||||||
|
DataTransfer, DataTransferId, DataTransferSend, SendData, TransferType, TypeHint, TypedData,
|
||||||
|
};
|
||||||
|
use winit_core::event::WindowEvent;
|
||||||
|
use winit_core::event_loop::DndAction;
|
||||||
|
use winit_core::window::WindowId;
|
||||||
|
|
||||||
|
use crate::make_data_transfer_id;
|
||||||
|
use crate::state::WinitState;
|
||||||
|
|
||||||
|
fn encode_uri_list<I>(uri_list: I) -> Vec<u8>
|
||||||
|
where
|
||||||
|
I: IntoIterator,
|
||||||
|
I::Item: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
for uri in uri_list {
|
||||||
|
out.extend_from_slice(OsStr::new(&uri).as_encoded_bytes());
|
||||||
|
out.extend_from_slice(b"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataSourceHandler for WinitState {
|
||||||
|
fn accept_mime(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &WlDataSource,
|
||||||
|
_: Option<String>,
|
||||||
|
) {
|
||||||
|
// This method isn't a necessary part of the protocol, it's a holdover from the first
|
||||||
|
// version of DnD in Wayland and now just serves as a hint.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_request(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &WlDataSource,
|
||||||
|
mime: String,
|
||||||
|
fd: WritePipe,
|
||||||
|
) {
|
||||||
|
let Some(data) = self.dnd_state.send_drag_data_mut() else {
|
||||||
|
// TODO: Is there a way to explicitly express that the data was not sent?
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mime = MimeType::parse(mime);
|
||||||
|
|
||||||
|
let Some(send_data) = data.data_for_type(&mime) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut encoder = match send_data {
|
||||||
|
SendData::Uris(strings) => Cursor::new(encode_uri_list(strings)),
|
||||||
|
SendData::String(str) => match mime.parse_charset() {
|
||||||
|
Ok(Charset::Utf8) => Cursor::new(str.into_bytes()),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("{e}");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SendData::Bytes(binary) => Cursor::new(binary),
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = self.loop_handle.insert_source(fd, move |_, file, _| {
|
||||||
|
// Safety: We only mutate `file` in-place and do not replace and drop it.
|
||||||
|
let file = unsafe { file.get_mut() };
|
||||||
|
loop {
|
||||||
|
let Ok(encoded_bytes) = encoder.fill_buf() else {
|
||||||
|
return PostAction::Remove;
|
||||||
|
};
|
||||||
|
|
||||||
|
match file.write(encoded_bytes) {
|
||||||
|
Ok(0) => {
|
||||||
|
break PostAction::Remove;
|
||||||
|
},
|
||||||
|
Ok(consumed) => {
|
||||||
|
encoder.consume(consumed);
|
||||||
|
},
|
||||||
|
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
||||||
|
break PostAction::Continue;
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
break PostAction::Remove;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancelled(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
|
||||||
|
let Some(current_drag) = self.dnd_state.send_drag() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_id = current_drag.window_id;
|
||||||
|
let id = current_drag.data_transfer_id;
|
||||||
|
|
||||||
|
self.events_sink.push_window_event(WindowEvent::OutgoingDragCanceled { id }, window_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dnd_dropped(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataSource) {
|
||||||
|
let Some(current_drag) = self.dnd_state.send_drag() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_id = current_drag.window_id;
|
||||||
|
let id = current_drag.data_transfer_id;
|
||||||
|
let selected_action = current_drag.selected_action;
|
||||||
|
|
||||||
|
self.events_sink.push_window_event(
|
||||||
|
WindowEvent::OutgoingDragDropped {
|
||||||
|
id,
|
||||||
|
action: dnd_action_wl_to_winit(selected_action),
|
||||||
|
},
|
||||||
|
window_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dnd_finished(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wayland_client::protocol::wl_data_source::WlDataSource,
|
||||||
|
) {
|
||||||
|
self.dnd_state.clear_send_drag();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &WlDataSource,
|
||||||
|
action: WlDndAction,
|
||||||
|
) {
|
||||||
|
self.dnd_state.set_target_drag_action(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)]
|
||||||
|
enum Charset {
|
||||||
|
#[default]
|
||||||
|
Utf8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// MIME type as string, with an optional hint detected from the MIME type.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
|
||||||
|
pub struct MimeType {
|
||||||
|
mime: Arc<str>,
|
||||||
|
hint: Option<TypeHint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIME types
|
||||||
|
// Files
|
||||||
|
const TEXT_URI_LIST: &str = "text/uri-list";
|
||||||
|
// Plaintext
|
||||||
|
const TEXT_PLAIN: &str = "text/plain";
|
||||||
|
const TEXT_PLAIN_CHARSET_UTF8: &str = "text/plain; charset=utf-8";
|
||||||
|
// HTML
|
||||||
|
const TEXT_HTML: &str = "text/html";
|
||||||
|
const TEXT_HTML_CHARSET_UTF8: &str = "text/html; charset=utf-8";
|
||||||
|
// RTF
|
||||||
|
const APPLICATION_RTF: &str = "application/rtf";
|
||||||
|
// Audio
|
||||||
|
const AUDIO_AAC: &str = "audio/aac";
|
||||||
|
const AUDIO_AIFF: &str = "audio/aiff";
|
||||||
|
const AUDIO_FLAC: &str = "audio/flac";
|
||||||
|
const AUDIO_WAV: &str = "audio/wav";
|
||||||
|
const AUDIO_WAVE: &str = "audio/wave";
|
||||||
|
const AUDIO_X_WAV: &str = "audio/x-wav";
|
||||||
|
const AUDIO_VND_WAV: &str = "audio/vnd.wav";
|
||||||
|
const AUDIO_VND_WAVE: &str = "audio/vnd.wave";
|
||||||
|
const AUDIO_MPEG: &str = "audio/mpeg";
|
||||||
|
const AUDIO_OGG: &str = "audio/ogg";
|
||||||
|
// Image
|
||||||
|
const IMAGE_BMP: &str = "image/bmp";
|
||||||
|
const IMAGE_GIF: &str = "image/gif";
|
||||||
|
const IMAGE_JPEG: &str = "image/jpeg";
|
||||||
|
const IMAGE_PJPEG: &str = "image/pjpeg";
|
||||||
|
const IMAGE_PNG: &str = "image/png";
|
||||||
|
const IMAGE_SVG: &str = "image/svg+xml";
|
||||||
|
const IMAGE_TIFF: &str = "image/tiff";
|
||||||
|
const IMAGE_WEBP: &str = "image/webp";
|
||||||
|
const IMAGE_X_ICON: &str = "image/x-icon";
|
||||||
|
const IMAGE_RAW: &str = "image/x-panasonic-raw";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct UnexpectedCharsetError<'a>(&'a str);
|
||||||
|
|
||||||
|
impl std::error::Error for UnexpectedCharsetError<'_> {}
|
||||||
|
|
||||||
|
impl fmt::Display for UnexpectedCharsetError<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "Unsupported charset: {}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MimeType {
|
||||||
|
const MIME_HINT_MAP: &[(&str, TypeHint)] = &[
|
||||||
|
// Files
|
||||||
|
(TEXT_URI_LIST, TypeHint::UriList),
|
||||||
|
// Plaintext
|
||||||
|
(TEXT_PLAIN, TypeHint::Plaintext),
|
||||||
|
(TEXT_PLAIN_CHARSET_UTF8, TypeHint::Plaintext),
|
||||||
|
// HTML
|
||||||
|
(TEXT_HTML, TypeHint::Html),
|
||||||
|
(TEXT_HTML_CHARSET_UTF8, TypeHint::Html),
|
||||||
|
// RTF
|
||||||
|
(APPLICATION_RTF, TypeHint::Rtf),
|
||||||
|
// Audio
|
||||||
|
(AUDIO_AAC, TypeHint::Audio { extension_hint: Some("aac") }),
|
||||||
|
(AUDIO_AIFF, TypeHint::Audio { extension_hint: Some("aif") }),
|
||||||
|
(AUDIO_FLAC, TypeHint::Audio { extension_hint: Some("flac") }),
|
||||||
|
(AUDIO_VND_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(AUDIO_VND_WAVE, TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(AUDIO_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(AUDIO_WAVE, TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(AUDIO_X_WAV, TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(AUDIO_OGG, TypeHint::Audio { extension_hint: Some("ogg") }),
|
||||||
|
(AUDIO_MPEG, TypeHint::Audio { extension_hint: Some("mp3") }),
|
||||||
|
// Image
|
||||||
|
(IMAGE_BMP, TypeHint::Image { extension_hint: Some("bmp") }),
|
||||||
|
(IMAGE_GIF, TypeHint::Image { extension_hint: Some("gif") }),
|
||||||
|
(IMAGE_JPEG, TypeHint::Image { extension_hint: Some("jpg") }),
|
||||||
|
(IMAGE_PJPEG, TypeHint::Image { extension_hint: Some("jpg") }),
|
||||||
|
(IMAGE_PNG, TypeHint::Image { extension_hint: Some("png") }),
|
||||||
|
(IMAGE_RAW, TypeHint::Image { extension_hint: Some("raw") }),
|
||||||
|
(IMAGE_SVG, TypeHint::Image { extension_hint: Some("svg") }),
|
||||||
|
(IMAGE_TIFF, TypeHint::Image { extension_hint: Some("tiff") }),
|
||||||
|
(IMAGE_WEBP, TypeHint::Image { extension_hint: Some("webp") }),
|
||||||
|
(IMAGE_X_ICON, TypeHint::Image { extension_hint: Some("ico") }),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Returns an iterator so that things like the multiple charsets for plaintext/HTML
|
||||||
|
// and the multiple ways of expressing .wav work correctly.
|
||||||
|
pub(crate) fn from_dyn(type_: &dyn TransferType) -> impl Iterator<Item = Self> {
|
||||||
|
let downcast = type_.cast_ref::<Self>().cloned();
|
||||||
|
let downcast_failed = downcast.is_none();
|
||||||
|
// This filter is a bit hacky, but it's the only way to ensure that we always
|
||||||
|
// return the same type.
|
||||||
|
let from_hint = downcast_failed
|
||||||
|
.then_some(
|
||||||
|
Self::MIME_HINT_MAP
|
||||||
|
.iter()
|
||||||
|
.filter(move |(_, haystack)| TransferType::matches(haystack, type_))
|
||||||
|
.map(move |(mime, _)| Self {
|
||||||
|
mime: mime.to_string().into(),
|
||||||
|
hint: type_.hint(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
downcast.into_iter().chain(from_hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We should properly parse MIME types using `mime` or a similar crate.
|
||||||
|
fn parse_charset(&self) -> Result<Charset, UnexpectedCharsetError<'_>> {
|
||||||
|
let Some((_, charset)) = self
|
||||||
|
.mime
|
||||||
|
.split_once(';')
|
||||||
|
.and_then(|(_essence, options)| options.split_once("charset="))
|
||||||
|
else {
|
||||||
|
return Ok(Default::default());
|
||||||
|
};
|
||||||
|
|
||||||
|
let charset = charset.split_once(',').map(|(first, _)| first).unwrap_or(charset).trim();
|
||||||
|
|
||||||
|
if charset == "utf-8" { Ok(Charset::Utf8) } else { Err(UnexpectedCharsetError(charset)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(mime: String) -> Self {
|
||||||
|
let hint = Self::MIME_HINT_MAP
|
||||||
|
.iter()
|
||||||
|
.find_map(|(haystack, hint)| (*haystack == &*mime).then_some(*hint))
|
||||||
|
.or_else(|| {
|
||||||
|
if mime.starts_with("image/") {
|
||||||
|
Some(TypeHint::Image { extension_hint: None })
|
||||||
|
} else if mime.starts_with("audio/") {
|
||||||
|
Some(TypeHint::Audio { extension_hint: None })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { mime: mime.into(), hint }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for MimeType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
self.mime.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub struct UnknownTypeHint(pub TypeHint);
|
||||||
|
|
||||||
|
impl fmt::Display for UnknownTypeHint {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "Unknown type hint: {:?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<TypeHint> for MimeType {
|
||||||
|
type Error = UnknownTypeHint;
|
||||||
|
|
||||||
|
fn try_from(hint: TypeHint) -> Result<Self, Self::Error> {
|
||||||
|
let mime = Self::MIME_HINT_MAP
|
||||||
|
.iter()
|
||||||
|
.find_map(|(mime, haystack)| (*haystack == hint).then_some(*mime))
|
||||||
|
.ok_or(UnknownTypeHint(hint))?;
|
||||||
|
|
||||||
|
Ok(Self { mime: mime.to_owned().into(), hint: Some(hint) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferType for MimeType {
|
||||||
|
fn hint(&self) -> Option<TypeHint> {
|
||||||
|
self.hint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, other: &dyn TransferType) -> bool {
|
||||||
|
if let Some(other_mime) = other.cast_ref::<Self>() {
|
||||||
|
*self == *other_mime
|
||||||
|
} else {
|
||||||
|
// If either hint is `None`, return false
|
||||||
|
self.hint().is_some_and(|hint| other.hint() == Some(hint))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BytesResult = Result<Vec<u8>, Arc<io::Error>>;
|
||||||
|
|
||||||
|
/// Typed data transfer from another application.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MimeData {
|
||||||
|
mime_type: MimeType,
|
||||||
|
result: BytesResult,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MimeData {
|
||||||
|
pub(crate) fn new(mime_type: MimeType, result: BytesResult) -> Self {
|
||||||
|
Self { mime_type, result }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data(&self) -> io::Result<&[u8]> {
|
||||||
|
fn arc_to_io_error(arc: Arc<io::Error>) -> io::Error {
|
||||||
|
io::Error::new(arc.kind(), arc)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.result.as_deref().map_err(|e| arc_to_io_error(e.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypedData for MimeData {
|
||||||
|
fn type_(&self) -> &dyn TransferType {
|
||||||
|
&self.mime_type
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
|
||||||
|
let data = self.data().ok()?.to_owned();
|
||||||
|
|
||||||
|
Some(Box::new(io::Cursor::new(data)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_as_bytes(&self) -> io::Result<Vec<u8>> {
|
||||||
|
self.data().map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_as_uris(&self) -> io::Result<Vec<String>> {
|
||||||
|
let data = self.data()?;
|
||||||
|
|
||||||
|
Cursor::new(&data)
|
||||||
|
.lines()
|
||||||
|
.filter(|result| match result {
|
||||||
|
Ok(s) => !s.starts_with('#'),
|
||||||
|
// We want to maintain errors, so the final `collect` returns an error too
|
||||||
|
Err(_) => true,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_as_string(&self) -> io::Result<String> {
|
||||||
|
let charset = self.mime_type.parse_charset();
|
||||||
|
|
||||||
|
let data = self.data()?;
|
||||||
|
|
||||||
|
match charset {
|
||||||
|
Ok(Charset::Utf8) => String::from_utf8(data.to_vec())
|
||||||
|
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err)),
|
||||||
|
Err(e) => Err(io::Error::other(e.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper around `WlDataOffer`, implementing `DataTransfer`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DataOffer {
|
||||||
|
mime_types: Arc<[MimeType]>,
|
||||||
|
data: WlDataOffer,
|
||||||
|
available_actions: WlDndAction,
|
||||||
|
data_device_id: ObjectId,
|
||||||
|
serial: u32,
|
||||||
|
window_id: WindowId,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dnd_action_winit_to_wl(winit: DndAction) -> WlDndAction {
|
||||||
|
match winit {
|
||||||
|
DndAction::Move => WlDndAction::Move,
|
||||||
|
DndAction::Copy => WlDndAction::Copy,
|
||||||
|
DndAction::Ask => WlDndAction::Ask,
|
||||||
|
_ => WlDndAction::empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dnd_action_wl_to_winit(wl: WlDndAction) -> Option<DndAction> {
|
||||||
|
match wl {
|
||||||
|
WlDndAction::Move => Some(DndAction::Move),
|
||||||
|
WlDndAction::Copy => Some(DndAction::Copy),
|
||||||
|
WlDndAction::Ask => Some(DndAction::Ask),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataOffer {
|
||||||
|
pub(crate) fn transfer_id(&self) -> DataTransferId {
|
||||||
|
make_data_transfer_id(self.data_device_id.clone(), self.serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn first_mime_type(&self) -> Option<&MimeType> {
|
||||||
|
self.mime_types.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn serial(&self) -> u32 {
|
||||||
|
self.serial
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn window_id(&self) -> WindowId {
|
||||||
|
self.window_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_actions(&self, action_set: &[DndAction]) -> bool {
|
||||||
|
let preferred_action = action_set.iter().find_map(|winit| {
|
||||||
|
let wl = dnd_action_winit_to_wl(*winit);
|
||||||
|
self.available_actions.intersects(wl).then_some(wl)
|
||||||
|
});
|
||||||
|
|
||||||
|
let any = preferred_action.is_some();
|
||||||
|
|
||||||
|
let all_actions = action_set
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(dnd_action_winit_to_wl)
|
||||||
|
.fold(WlDndAction::empty(), BitOr::bitor);
|
||||||
|
|
||||||
|
self.data.set_actions(all_actions, preferred_action.unwrap_or(WlDndAction::empty()));
|
||||||
|
|
||||||
|
any
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn find_type_dyn<'a>(&'a self, type_: &'a dyn TransferType) -> Option<&'a MimeType> {
|
||||||
|
match type_.cast_ref::<MimeType>() {
|
||||||
|
Some(mime_type) => Some(mime_type),
|
||||||
|
None => {
|
||||||
|
let hint = type_.hint()?;
|
||||||
|
self.mime_types.iter().find(|mime_type| {
|
||||||
|
mime_type.hint().is_some_and(|haystack| haystack.matches(&hint))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for DataOffer {
|
||||||
|
type Target = WlDataOffer;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataTransfer for DataOffer {
|
||||||
|
fn for_each_available_type<'this>(
|
||||||
|
&'this self,
|
||||||
|
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>,
|
||||||
|
) {
|
||||||
|
let _ = self.mime_types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper for [`WlDataSource`], which exposes the types that are advertised by a data
|
||||||
|
/// transfer operation, along with the data that the source represents
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DragSource {
|
||||||
|
pub(crate) data_transfer_id: DataTransferId,
|
||||||
|
/// The `WlDataSource` generated from `data`.
|
||||||
|
///
|
||||||
|
/// This is stored internally, as if this source is dropped then the
|
||||||
|
/// drag operation will be cancelled.
|
||||||
|
_data_source: SctkDragSource,
|
||||||
|
/// The supplied [`DataTransferSend`].
|
||||||
|
pub(crate) data: Box<dyn DataTransferSend>,
|
||||||
|
pub(crate) selected_action: WlDndAction,
|
||||||
|
pub(crate) window_id: WindowId,
|
||||||
|
/// (Optionally) an icon for the drag-and-drop operation.
|
||||||
|
_icon: Option<WlSurface>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DragSource {
|
||||||
|
pub(crate) fn new(
|
||||||
|
data_transfer_id: DataTransferId,
|
||||||
|
data_source: SctkDragSource,
|
||||||
|
data: Box<dyn DataTransferSend>,
|
||||||
|
icon: Option<WlSurface>,
|
||||||
|
window_id: WindowId,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
data_transfer_id,
|
||||||
|
_data_source: data_source,
|
||||||
|
data,
|
||||||
|
selected_action: WlDndAction::None,
|
||||||
|
window_id,
|
||||||
|
_icon: icon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-type data to be sent. See [`DataTransferSend`].
|
||||||
|
pub fn data(&mut self) -> &mut dyn DataTransferSend {
|
||||||
|
&mut *self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current state of an in-progress drag-and-drop operation.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct DndState {
|
||||||
|
receive_drag: Option<DataOffer>,
|
||||||
|
send_drag: Option<DragSource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DndState {
|
||||||
|
pub(crate) fn receive_drag(&self) -> Option<&DataOffer> {
|
||||||
|
self.receive_drag.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_send_drag(&mut self, source: DragSource) {
|
||||||
|
self.send_drag = Some(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send_drag(&self) -> Option<&DragSource> {
|
||||||
|
self.send_drag.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_target_drag_action(&mut self, action: WlDndAction) {
|
||||||
|
if let Some(source) = &mut self.send_drag {
|
||||||
|
source.selected_action = action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if a drag operation was in progress, `false` if no drag operation was in
|
||||||
|
/// progress.
|
||||||
|
pub(crate) fn clear_send_drag(&mut self) -> bool {
|
||||||
|
self.send_drag.take().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send_drag_data_mut(&mut self) -> Option<&mut dyn DataTransferSend> {
|
||||||
|
self.send_drag.as_mut().map(|send| send.data())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataOfferHandler for WinitState {
|
||||||
|
fn source_actions(
|
||||||
|
&mut self,
|
||||||
|
conn: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
offer: &mut DragOffer,
|
||||||
|
actions: WlDndAction,
|
||||||
|
) {
|
||||||
|
let _ = actions;
|
||||||
|
let _ = offer;
|
||||||
|
let _ = qh;
|
||||||
|
let _ = conn;
|
||||||
|
// Not implemented, but required for `DataDeviceHandler`.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_action(
|
||||||
|
&mut self,
|
||||||
|
conn: &Connection,
|
||||||
|
qh: &QueueHandle<Self>,
|
||||||
|
offer: &mut DragOffer,
|
||||||
|
actions: WlDndAction,
|
||||||
|
) {
|
||||||
|
let _ = actions;
|
||||||
|
let _ = offer;
|
||||||
|
let _ = qh;
|
||||||
|
let _ = conn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataDeviceHandler for WinitState {
|
||||||
|
fn enter(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
data_device: &WlDataDevice,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
wl_surface: &WlSurface,
|
||||||
|
) {
|
||||||
|
let Some(data) = data_device.data::<DataDeviceData>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(drag) = data.drag_offer() else {
|
||||||
|
// Selections are not yet implemented
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_id = crate::make_wid(wl_surface);
|
||||||
|
|
||||||
|
let current_drag = drag.with_mime_types(|types| DataOffer {
|
||||||
|
mime_types: types
|
||||||
|
.iter()
|
||||||
|
.map(|str| MimeType::parse(str.clone()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into(),
|
||||||
|
available_actions: drag.source_actions,
|
||||||
|
serial: drag.serial,
|
||||||
|
data_device_id: data_device.id(),
|
||||||
|
data: drag.inner().clone(),
|
||||||
|
window_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
current_drag.set_actions(&[]);
|
||||||
|
|
||||||
|
let id = current_drag.transfer_id();
|
||||||
|
|
||||||
|
self.dnd_state.receive_drag = Some(current_drag);
|
||||||
|
|
||||||
|
let scale_factor = self
|
||||||
|
.windows
|
||||||
|
.borrow()
|
||||||
|
.get(&window_id)
|
||||||
|
.map(|window| window.lock().unwrap().scale_factor())
|
||||||
|
.unwrap_or(1.);
|
||||||
|
let position: PhysicalPosition<f64> = LogicalPosition::new(x, y).to_physical(scale_factor);
|
||||||
|
|
||||||
|
self.events_sink.push_window_event(
|
||||||
|
WindowEvent::DragEntered { id, position: Some(position) },
|
||||||
|
window_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn leave(&mut self, _: &Connection, _: &QueueHandle<Self>, data_device: &WlDataDevice) {
|
||||||
|
let Some(data) = data_device.data::<DataDeviceData>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(current_drag) = self.dnd_state.receive_drag() {
|
||||||
|
self.events_sink.push_window_event(
|
||||||
|
WindowEvent::DragLeft { id: current_drag.transfer_id() },
|
||||||
|
current_drag.window_id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(receive_drag) = self.dnd_state.receive_drag.take() {
|
||||||
|
receive_drag.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(drag) = data.drag_offer() {
|
||||||
|
drag.destroy();
|
||||||
|
}
|
||||||
|
if let Some(selection) = data.selection_offer() {
|
||||||
|
selection.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn motion(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
data_device: &WlDataDevice,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
) {
|
||||||
|
let Some(data) = data_device.data::<DataDeviceData>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(drag) = data.drag_offer() else {
|
||||||
|
// Selections (copy/paste) are not yet implemented
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// `selected_action` should only contain a single flag, but we check with `contains`
|
||||||
|
// just in case we or the compositor misunderstood the spec.
|
||||||
|
let proposed_action = if drag.selected_action.contains(WlDndAction::Move) {
|
||||||
|
Some(DndAction::Move)
|
||||||
|
} else if drag.selected_action.contains(WlDndAction::Copy) {
|
||||||
|
Some(DndAction::Copy)
|
||||||
|
} else if drag.selected_action.contains(WlDndAction::Ask) {
|
||||||
|
Some(DndAction::Ask)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(current_drag) = self.dnd_state.receive_drag() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_id = crate::make_wid(&drag.surface);
|
||||||
|
|
||||||
|
let scale_factor = self
|
||||||
|
.windows
|
||||||
|
.borrow()
|
||||||
|
.get(&window_id)
|
||||||
|
.map(|window| window.lock().unwrap().scale_factor())
|
||||||
|
.unwrap_or(1.);
|
||||||
|
let position: PhysicalPosition<f64> = LogicalPosition::new(x, y).to_physical(scale_factor);
|
||||||
|
|
||||||
|
self.events_sink.push_window_event(
|
||||||
|
WindowEvent::DragPosition { id: current_drag.transfer_id(), position, proposed_action },
|
||||||
|
window_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selection(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &WlDataDevice) {
|
||||||
|
// We don't handle selections right now.
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drop_performed(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
data_device: &WlDataDevice,
|
||||||
|
) {
|
||||||
|
let Some(data) = data_device.data::<DataDeviceData>() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(drag) = data.drag_offer() else {
|
||||||
|
// Selections (copy/paste) are not yet implemented
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(current_drag) = self.dnd_state.receive_drag() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_id = crate::make_wid(&drag.surface);
|
||||||
|
|
||||||
|
// `selected_action` should only contain a single flag, but we check with `contains`
|
||||||
|
// just in case we or the compositor misunderstood the spec.
|
||||||
|
let proposed_action = if drag.selected_action.contains(WlDndAction::Move) {
|
||||||
|
Some(DndAction::Move)
|
||||||
|
} else if drag.selected_action.contains(WlDndAction::Copy) {
|
||||||
|
Some(DndAction::Copy)
|
||||||
|
} else if drag.selected_action.contains(WlDndAction::Ask) {
|
||||||
|
Some(DndAction::Ask)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
self.events_sink.push_window_event(
|
||||||
|
WindowEvent::DragDropped { id: current_drag.transfer_id(), proposed_action },
|
||||||
|
window_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(receive_drag) = self.dnd_state.receive_drag.take() {
|
||||||
|
receive_drag.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(drag) = data.drag_offer() {
|
||||||
|
drag.destroy();
|
||||||
|
}
|
||||||
|
if let Some(selection) = data.selection_offer() {
|
||||||
|
selection.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sctk::delegate_data_device!(WinitState);
|
||||||
@@ -1,35 +1,46 @@
|
|||||||
//! The event-loop routines.
|
//! The event-loop routines.
|
||||||
|
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::io::Result as IOResult;
|
use std::io::{self, Read, Result as IOResult};
|
||||||
use std::mem;
|
use std::ops::BitOr;
|
||||||
use std::os::fd::OwnedFd;
|
use std::os::fd::OwnedFd;
|
||||||
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd};
|
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, RawFd};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Condvar, Mutex};
|
use std::sync::{Arc, Condvar, Mutex};
|
||||||
use std::thread::JoinHandle;
|
use std::thread::JoinHandle;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use std::{fmt, mem};
|
||||||
|
|
||||||
|
use calloop::PostAction;
|
||||||
use calloop::ping::Ping;
|
use calloop::ping::Ping;
|
||||||
use dpi::LogicalSize;
|
use dpi::LogicalSize;
|
||||||
use rustix::event::{PollFd, PollFlags};
|
use rustix::event::{PollFd, PollFlags};
|
||||||
use rustix::pipe::{self, PipeFlags};
|
use rustix::pipe::{self, PipeFlags};
|
||||||
|
use sctk::data_device_manager::{ReadPipe, data_offer};
|
||||||
use sctk::reexports::calloop_wayland_source::WaylandSource;
|
use sctk::reexports::calloop_wayland_source::WaylandSource;
|
||||||
use sctk::reexports::client::{Connection, QueueHandle, globals};
|
use sctk::reexports::client::{Connection, QueueHandle, globals};
|
||||||
|
use sctk::shell::WaylandSurface;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
use wayland_client::Proxy;
|
||||||
|
use wayland_client::protocol::wl_data_device_manager::DndAction as WlDndAction;
|
||||||
|
use wayland_client::protocol::wl_shm::Format;
|
||||||
use winit_core::application::ApplicationHandler;
|
use winit_core::application::ApplicationHandler;
|
||||||
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
|
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
|
||||||
|
use winit_core::data_transfer::{DataTransfer, DataTransferId, DataTransferSend, TransferType};
|
||||||
use winit_core::error::{EventLoopError, NotSupportedError, OsError, RequestError};
|
use winit_core::error::{EventLoopError, NotSupportedError, OsError, RequestError};
|
||||||
use winit_core::event::{DeviceEvent, StartCause, SurfaceSizeWriter, WindowEvent};
|
use winit_core::event::{DeviceEvent, StartCause, SurfaceSizeWriter, WindowEvent};
|
||||||
use winit_core::event_loop::pump_events::PumpStatus;
|
use winit_core::event_loop::pump_events::PumpStatus;
|
||||||
use winit_core::event_loop::{
|
use winit_core::event_loop::{
|
||||||
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
|
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
|
||||||
OwnedDisplayHandle as CoreOwnedDisplayHandle,
|
DndAction, DragIcon, OwnedDisplayHandle as CoreOwnedDisplayHandle,
|
||||||
};
|
};
|
||||||
|
use winit_core::icon::RgbaIcon;
|
||||||
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
|
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
|
||||||
use winit_core::window::Theme;
|
use winit_core::window::Theme;
|
||||||
|
|
||||||
|
use crate::dnd::{MimeData, dnd_action_winit_to_wl};
|
||||||
use crate::types::cursor::WaylandCustomCursor;
|
use crate::types::cursor::WaylandCustomCursor;
|
||||||
|
use crate::{DragSource, MimeType, image_to_buffer, make_data_transfer_id};
|
||||||
|
|
||||||
mod proxy;
|
mod proxy;
|
||||||
pub mod sink;
|
pub mod sink;
|
||||||
@@ -678,8 +689,233 @@ impl RootActiveEventLoop for ActiveEventLoop {
|
|||||||
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
|
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fetch_data_transfer(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
type_: &dyn TransferType,
|
||||||
|
) -> Result<AsyncRequestSerial, RequestError> {
|
||||||
|
let state = self.state.borrow_mut();
|
||||||
|
let Some(current_drag) = state.dnd_state.receive_drag() else {
|
||||||
|
return Err(RequestError::Ignored);
|
||||||
|
};
|
||||||
|
|
||||||
|
if current_drag.transfer_id() != id {
|
||||||
|
return Err(RequestError::Ignored);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(mime_type) = current_drag.find_type_dyn(type_) else {
|
||||||
|
return Err(RequestError::Ignored);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mime_type_str = mime_type.to_string();
|
||||||
|
|
||||||
|
// create a pipe
|
||||||
|
let (readfd, writefd) =
|
||||||
|
pipe::pipe_with(PipeFlags::CLOEXEC | PipeFlags::NONBLOCK).map_err(|e| os_error!(e))?;
|
||||||
|
|
||||||
|
let async_request_serial = AsyncRequestSerial::get();
|
||||||
|
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
let window_id = current_drag.window_id();
|
||||||
|
let mut mime_type = Some(mime_type.clone());
|
||||||
|
|
||||||
|
let _ = state.loop_handle.insert_source(ReadPipe::from(readfd), move |_, file, state| {
|
||||||
|
// SAFETY: We do not overwrite the referent of `file`
|
||||||
|
let file = unsafe { file.get_mut() };
|
||||||
|
|
||||||
|
let result = match file.read_to_end(&mut buffer) {
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||||
|
return PostAction::Continue;
|
||||||
|
},
|
||||||
|
Ok(0) => Ok(mem::take(&mut buffer)),
|
||||||
|
Ok(_) => {
|
||||||
|
return PostAction::Continue;
|
||||||
|
},
|
||||||
|
Err(e) => Err(Arc::new(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
state.events_sink.push_window_event(
|
||||||
|
WindowEvent::DataTransferReceived {
|
||||||
|
id,
|
||||||
|
serial: async_request_serial,
|
||||||
|
// `unwrap` is safe here, as we always return `PostAction::Remove` in this
|
||||||
|
// branch.
|
||||||
|
value: Arc::new(MimeData::new(mime_type.take().unwrap(), result)),
|
||||||
|
},
|
||||||
|
window_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
PostAction::Remove
|
||||||
|
});
|
||||||
|
|
||||||
|
current_drag.accept(current_drag.serial(), Some(mime_type_str.clone()));
|
||||||
|
data_offer::receive_to_fd(current_drag, mime_type_str, writefd);
|
||||||
|
|
||||||
|
Ok(async_request_serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
|
||||||
|
let state = self.state.borrow();
|
||||||
|
let Some(state) = state.dnd_state.receive_drag() else {
|
||||||
|
return Err(RequestError::Ignored);
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.transfer_id() != id {
|
||||||
|
return Err(RequestError::Ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Box::new(state.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_valid_dnd_actions(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
actions: &[DndAction],
|
||||||
|
) -> Result<(), RequestError> {
|
||||||
|
let state = self.state.borrow();
|
||||||
|
let Some(state) = state.dnd_state.receive_drag() else {
|
||||||
|
return Err(os_error!(UnknownDataTransfer(id)).into());
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.transfer_id() != id {
|
||||||
|
return Err(os_error!(UnknownDataTransfer(id)).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let any_actions = state.set_actions(actions);
|
||||||
|
let accepted_type =
|
||||||
|
if any_actions { state.first_mime_type().map(|mime| mime.to_string()) } else { None };
|
||||||
|
// Some compositors won't even send the "dropped" event if no type
|
||||||
|
// has been accepted, so we need to accept _something_ here. The
|
||||||
|
// application can accept further types by fetching the data, but
|
||||||
|
// this will at least mean that waiting until the drop to start
|
||||||
|
// fetching data won't prevent the drop from working at all.
|
||||||
|
state.accept(state.serial(), accepted_type);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_drag(
|
||||||
|
&self,
|
||||||
|
source: WindowId,
|
||||||
|
send_data: Box<dyn DataTransferSend>,
|
||||||
|
action_mask: &[DndAction],
|
||||||
|
icon: Option<DragIcon>,
|
||||||
|
) -> Result<DataTransferId, RequestError> {
|
||||||
|
const NO_POINTER_CAP_ERROR_MSG: &str =
|
||||||
|
"Tried to initiate drag, but source window does not have the pointer capability";
|
||||||
|
|
||||||
|
let mut state = self.state.borrow_mut();
|
||||||
|
let dnd_actions = action_mask
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(dnd_action_winit_to_wl)
|
||||||
|
.fold(WlDndAction::empty(), BitOr::bitor);
|
||||||
|
|
||||||
|
let data_device_manager = state
|
||||||
|
.data_device_manager_state
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(NotSupportedError::new("Tried to initiate drag, but data device not enabled"))?;
|
||||||
|
|
||||||
|
let mut mime_types = Vec::new();
|
||||||
|
send_data.for_each_available_type(&mut |ty_| {
|
||||||
|
for mime in MimeType::from_dyn(ty_) {
|
||||||
|
mime_types.push(mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ops::ControlFlow::Continue(())
|
||||||
|
});
|
||||||
|
|
||||||
|
let data_source = data_device_manager.create_drag_and_drop_source(
|
||||||
|
&self.queue_handle,
|
||||||
|
mime_types,
|
||||||
|
dnd_actions,
|
||||||
|
);
|
||||||
|
|
||||||
|
let icon_surface = {
|
||||||
|
let mut pool = state.image_pool.lock().unwrap();
|
||||||
|
icon.and_then(|icon| {
|
||||||
|
let rgba = icon.icon.cast_ref::<RgbaIcon>()?;
|
||||||
|
|
||||||
|
let width = rgba.width().try_into().ok()?;
|
||||||
|
let height = rgba.height().try_into().ok()?;
|
||||||
|
|
||||||
|
let buffer =
|
||||||
|
image_to_buffer(width, height, rgba.buffer(), Format::Argb8888, &mut pool)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let surface = state.compositor_state.create_surface(&self.queue_handle);
|
||||||
|
if surface.version() >= 5 {
|
||||||
|
buffer.attach_to(&surface).ok()?;
|
||||||
|
surface.offset(icon.offset_x, icon.offset_y);
|
||||||
|
} else {
|
||||||
|
surface.attach(Some(buffer.wl_buffer()), icon.offset_x, icon.offset_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(surface)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// New scope to ensure we drop the locks as soon as possible.
|
||||||
|
let transfer_id = {
|
||||||
|
let windows = state.windows.borrow();
|
||||||
|
let source_window_mutex = windows
|
||||||
|
.get(&source)
|
||||||
|
.ok_or(os_error!("Tried to initiate drag, but source window ID was invalid"))?;
|
||||||
|
let source_window_state = source_window_mutex.lock().unwrap();
|
||||||
|
let source_surface = source_window_state.window.wl_surface();
|
||||||
|
|
||||||
|
let seat = source_window_state
|
||||||
|
.focused_seats()
|
||||||
|
.find_map(|seat_id| {
|
||||||
|
// HACK: How do we get the correct seat for pointers here?
|
||||||
|
state.seats.get(seat_id).filter(|seat| seat.data_device().is_some())
|
||||||
|
})
|
||||||
|
.ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?;
|
||||||
|
let data_device =
|
||||||
|
seat.data_device().ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?;
|
||||||
|
|
||||||
|
let serial = seat
|
||||||
|
.pointer_data()
|
||||||
|
.ok_or(NotSupportedError::new(NO_POINTER_CAP_ERROR_MSG))?
|
||||||
|
.latest_button_serial();
|
||||||
|
|
||||||
|
data_source.start_drag(data_device, source_surface, icon_surface.as_ref(), serial);
|
||||||
|
|
||||||
|
make_data_transfer_id(data_device.inner().id(), serial)
|
||||||
|
};
|
||||||
|
|
||||||
|
// For some reason, if we commit before starting the drag then the offset isn't applied.
|
||||||
|
// This doesn't seem to be documented anywhere, and it's possible that it's a bug in KDE.
|
||||||
|
if let Some(surface) = &icon_surface {
|
||||||
|
surface.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.dnd_state.set_send_drag(DragSource::new(
|
||||||
|
transfer_id,
|
||||||
|
data_source,
|
||||||
|
send_data,
|
||||||
|
icon_surface,
|
||||||
|
source,
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(transfer_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An operation was attempted on a data transfer ID, but that ID was invalid.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub struct UnknownDataTransfer(pub DataTransferId);
|
||||||
|
|
||||||
|
impl fmt::Display for UnknownDataTransfer {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let id = self.0.into_raw();
|
||||||
|
write!(f, "Unknown data transfer with ID {id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for UnknownDataTransfer {}
|
||||||
|
|
||||||
impl ActiveEventLoop {
|
impl ActiveEventLoop {
|
||||||
fn clear_exit(&self) {
|
fn clear_exit(&self) {
|
||||||
self.exit.set(None)
|
self.exit.set(None)
|
||||||
|
|||||||
@@ -18,13 +18,16 @@
|
|||||||
#![allow(clippy::mutable_key_type)]
|
#![allow(clippy::mutable_key_type)]
|
||||||
|
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
use std::hash::BuildHasher;
|
||||||
use std::ptr::NonNull;
|
use std::ptr::NonNull;
|
||||||
|
|
||||||
use dpi::{LogicalSize, PhysicalSize};
|
use dpi::{LogicalSize, PhysicalSize};
|
||||||
use sctk::reexports::client::Proxy;
|
use sctk::reexports::client::Proxy;
|
||||||
|
use sctk::reexports::client::backend::ObjectId;
|
||||||
use sctk::reexports::client::protocol::wl_surface::WlSurface;
|
use sctk::reexports::client::protocol::wl_surface::WlSurface;
|
||||||
use sctk::shm::slot::{Buffer, CreateBufferError, SlotPool};
|
use sctk::shm::slot::{Buffer, CreateBufferError, SlotPool};
|
||||||
use wayland_client::protocol::wl_shm::Format;
|
use wayland_client::protocol::wl_shm::Format;
|
||||||
|
use winit_core::data_transfer::DataTransferId;
|
||||||
use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop;
|
use winit_core::event_loop::ActiveEventLoop as CoreActiveEventLoop;
|
||||||
use winit_core::window::{
|
use winit_core::window::{
|
||||||
ActivationToken, PlatformWindowAttributes, Window as CoreWindow, WindowId,
|
ActivationToken, PlatformWindowAttributes, Window as CoreWindow, WindowId,
|
||||||
@@ -34,6 +37,7 @@ macro_rules! os_error {
|
|||||||
($error:expr) => {{ winit_core::error::OsError::new(line!(), file!(), $error) }};
|
($error:expr) => {{ winit_core::error::OsError::new(line!(), file!(), $error) }};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod dnd;
|
||||||
mod event_loop;
|
mod event_loop;
|
||||||
mod output;
|
mod output;
|
||||||
mod seat;
|
mod seat;
|
||||||
@@ -41,6 +45,7 @@ mod state;
|
|||||||
mod types;
|
mod types;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
||||||
|
pub use self::dnd::{DataOffer, DragSource, MimeData, MimeType};
|
||||||
pub use self::event_loop::{ActiveEventLoop, EventLoop};
|
pub use self::event_loop::{ActiveEventLoop, EventLoop};
|
||||||
pub use self::window::Window;
|
pub use self::window::Window;
|
||||||
|
|
||||||
@@ -149,6 +154,17 @@ fn make_wid(surface: &WlSurface) -> WindowId {
|
|||||||
WindowId::from_raw(surface.id().as_ptr() as usize)
|
WindowId::from_raw(surface.id().as_ptr() as usize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a `DataTransferId` for the given data device and serial.
|
||||||
|
///
|
||||||
|
/// It's currently unclear if this will result in the same ID when transferring to the same
|
||||||
|
/// application.
|
||||||
|
#[inline]
|
||||||
|
fn make_data_transfer_id(data_device_id: ObjectId, serial: u32) -> DataTransferId {
|
||||||
|
const BUILD_HASHER: foldhash::fast::FixedState = foldhash::fast::FixedState::with_seed(0);
|
||||||
|
|
||||||
|
DataTransferId::from_raw(BUILD_HASHER.hash_one((data_device_id, serial)) as i64)
|
||||||
|
}
|
||||||
|
|
||||||
/// The default routine does floor, but we need round on Wayland.
|
/// The default routine does floor, but we need round on Wayland.
|
||||||
fn logical_to_physical_rounded(size: LogicalSize<u32>, scale_factor: f64) -> PhysicalSize<u32> {
|
fn logical_to_physical_rounded(size: LogicalSize<u32>, scale_factor: f64) -> PhysicalSize<u32> {
|
||||||
let width = size.width as f64 * scale_factor;
|
let width = size.width as f64 * scale_factor;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use foldhash::HashMap;
|
use foldhash::HashMap;
|
||||||
|
use sctk::data_device_manager::data_device::DataDevice;
|
||||||
use sctk::reexports::client::backend::ObjectId;
|
use sctk::reexports::client::backend::ObjectId;
|
||||||
use sctk::reexports::client::protocol::wl_seat::WlSeat;
|
use sctk::reexports::client::protocol::wl_seat::WlSeat;
|
||||||
use sctk::reexports::client::protocol::wl_touch::WlTouch;
|
use sctk::reexports::client::protocol::wl_touch::WlTouch;
|
||||||
@@ -64,6 +65,9 @@ pub struct WinitSeatState {
|
|||||||
/// The hold pointer gesture bound on the seat.
|
/// The hold pointer gesture bound on the seat.
|
||||||
pointer_gesture_hold: Option<ZwpPointerGestureHoldV1>,
|
pointer_gesture_hold: Option<ZwpPointerGestureHoldV1>,
|
||||||
|
|
||||||
|
/// The drag-and-drop state
|
||||||
|
data_device: Option<DataDevice>,
|
||||||
|
|
||||||
/// The keyboard bound on the seat.
|
/// The keyboard bound on the seat.
|
||||||
keyboard_state: Option<KeyboardState>,
|
keyboard_state: Option<KeyboardState>,
|
||||||
|
|
||||||
@@ -78,6 +82,14 @@ impl WinitSeatState {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn data_device(&self) -> Option<&DataDevice> {
|
||||||
|
self.data_device.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pointer_data(&self) -> Option<&WinitPointerData> {
|
||||||
|
self.pointer.as_ref().and_then(|pointer| pointer.pointer().data())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SeatHandler for WinitState {
|
impl SeatHandler for WinitState {
|
||||||
@@ -129,6 +141,11 @@ impl SeatHandler for WinitState {
|
|||||||
)
|
)
|
||||||
.expect("failed to create pointer with present capability.");
|
.expect("failed to create pointer with present capability.");
|
||||||
|
|
||||||
|
seat_state.data_device = self
|
||||||
|
.data_device_manager_state
|
||||||
|
.as_ref()
|
||||||
|
.map(|device| device.get_data_device(queue_handle, &seat));
|
||||||
|
|
||||||
seat_state.relative_pointer = self.relative_pointer.as_ref().map(|manager| {
|
seat_state.relative_pointer = self.relative_pointer.as_ref().map(|manager| {
|
||||||
manager.get_relative_pointer(
|
manager.get_relative_pointer(
|
||||||
themed_pointer.pointer(),
|
themed_pointer.pointer(),
|
||||||
@@ -225,6 +242,8 @@ impl SeatHandler for WinitState {
|
|||||||
pointer_gesture_hold.destroy();
|
pointer_gesture_hold.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seat_state.data_device = None;
|
||||||
|
|
||||||
if let Some(pointer) = seat_state.pointer.take() {
|
if let Some(pointer) = seat_state.pointer.take() {
|
||||||
let pointer_data = pointer.pointer().winit_data();
|
let pointer_data = pointer.pointer().winit_data();
|
||||||
|
|
||||||
|
|||||||
@@ -22,19 +22,19 @@ use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport;
|
|||||||
|
|
||||||
use sctk::compositor::SurfaceData;
|
use sctk::compositor::SurfaceData;
|
||||||
use sctk::globals::GlobalData;
|
use sctk::globals::GlobalData;
|
||||||
|
use sctk::seat::SeatState;
|
||||||
use sctk::seat::pointer::{
|
use sctk::seat::pointer::{
|
||||||
PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler,
|
PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler,
|
||||||
};
|
};
|
||||||
use sctk::seat::SeatState;
|
|
||||||
|
|
||||||
use dpi::{LogicalPosition, PhysicalPosition};
|
use dpi::{LogicalPosition, PhysicalPosition};
|
||||||
use winit_core::event::{
|
use winit_core::event::{
|
||||||
ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource, TouchPhase,
|
ButtonSource, ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource,
|
||||||
WindowEvent, ButtonSource,
|
TouchPhase, WindowEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::state::WinitState;
|
|
||||||
use crate::WindowId;
|
use crate::WindowId;
|
||||||
|
use crate::state::WinitState;
|
||||||
|
|
||||||
pub mod pointer_gesture;
|
pub mod pointer_gesture;
|
||||||
pub mod relative_pointer;
|
pub mod relative_pointer;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use sctk::reexports::client::globals::{BindError, GlobalList};
|
use sctk::reexports::client::globals::{BindError, GlobalList};
|
||||||
use sctk::reexports::client::{delegate_dispatch, Dispatch};
|
|
||||||
use sctk::reexports::client::{Connection, QueueHandle};
|
use sctk::reexports::client::{Connection, QueueHandle};
|
||||||
|
use sctk::reexports::client::{Dispatch, delegate_dispatch};
|
||||||
use sctk::reexports::protocols::wp::relative_pointer::zv1::{
|
use sctk::reexports::protocols::wp::relative_pointer::zv1::{
|
||||||
client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
|
client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
|
||||||
client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
|
client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
|
||||||
@@ -12,8 +12,8 @@ use sctk::reexports::protocols::wp::relative_pointer::zv1::{
|
|||||||
|
|
||||||
use sctk::globals::GlobalData;
|
use sctk::globals::GlobalData;
|
||||||
|
|
||||||
use winit_core::event::DeviceEvent;
|
|
||||||
use crate::state::WinitState;
|
use crate::state::WinitState;
|
||||||
|
use winit_core::event::DeviceEvent;
|
||||||
|
|
||||||
/// Wrapper around the relative pointer.
|
/// Wrapper around the relative pointer.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
|
|
||||||
use foldhash::HashMap;
|
use foldhash::HashMap;
|
||||||
use sctk::compositor::{CompositorHandler, CompositorState};
|
use sctk::compositor::{CompositorHandler, CompositorState};
|
||||||
|
use sctk::data_device_manager::DataDeviceManagerState;
|
||||||
use sctk::output::{OutputHandler, OutputState};
|
use sctk::output::{OutputHandler, OutputState};
|
||||||
use sctk::reexports::calloop::LoopHandle;
|
use sctk::reexports::calloop::LoopHandle;
|
||||||
use sctk::reexports::client::backend::ObjectId;
|
use sctk::reexports::client::backend::ObjectId;
|
||||||
@@ -23,6 +24,7 @@ use sctk::subcompositor::SubcompositorState;
|
|||||||
use winit_core::error::OsError;
|
use winit_core::error::OsError;
|
||||||
|
|
||||||
use crate::WindowId;
|
use crate::WindowId;
|
||||||
|
use crate::dnd::DndState;
|
||||||
use crate::event_loop::sink::EventSink;
|
use crate::event_loop::sink::EventSink;
|
||||||
use crate::output::MonitorHandle;
|
use crate::output::MonitorHandle;
|
||||||
use crate::seat::{
|
use crate::seat::{
|
||||||
@@ -113,12 +115,18 @@ pub struct WinitState {
|
|||||||
/// Viewporter state on the given window.
|
/// Viewporter state on the given window.
|
||||||
pub viewporter_state: Option<ViewporterState>,
|
pub viewporter_state: Option<ViewporterState>,
|
||||||
|
|
||||||
|
/// Data device manager state on the given window.
|
||||||
|
pub data_device_manager_state: Option<DataDeviceManagerState>,
|
||||||
|
|
||||||
/// Fractional scaling manager.
|
/// Fractional scaling manager.
|
||||||
pub fractional_scaling_manager: Option<FractionalScalingManager>,
|
pub fractional_scaling_manager: Option<FractionalScalingManager>,
|
||||||
|
|
||||||
/// Blur manager.
|
/// Blur manager.
|
||||||
pub blur_manager: Option<BgrEffectManager>,
|
pub blur_manager: Option<BgrEffectManager>,
|
||||||
|
|
||||||
|
/// Drag-and-drop state.
|
||||||
|
pub dnd_state: DndState,
|
||||||
|
|
||||||
/// Loop handle to re-register event sources, such as keyboard repeat.
|
/// Loop handle to re-register event sources, such as keyboard repeat.
|
||||||
pub loop_handle: LoopHandle<'static, Self>,
|
pub loop_handle: LoopHandle<'static, Self>,
|
||||||
|
|
||||||
@@ -168,6 +176,17 @@ impl WinitState {
|
|||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let data_device_manager_state = match DataDeviceManagerState::bind(globals, queue_handle) {
|
||||||
|
Ok(state) => Some(state),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"Data device manager not available, clipboard and drag-and-drop disabled: \
|
||||||
|
{e:?}"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let shm = Shm::bind(globals, queue_handle).map_err(|err| os_error!(err))?;
|
let shm = Shm::bind(globals, queue_handle).map_err(|err| os_error!(err))?;
|
||||||
let image_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap()));
|
let image_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap()));
|
||||||
|
|
||||||
@@ -191,9 +210,12 @@ impl WinitState {
|
|||||||
window_compositor_updates: Vec::new(),
|
window_compositor_updates: Vec::new(),
|
||||||
window_events_sink: Default::default(),
|
window_events_sink: Default::default(),
|
||||||
viewporter_state,
|
viewporter_state,
|
||||||
|
data_device_manager_state,
|
||||||
fractional_scaling_manager,
|
fractional_scaling_manager,
|
||||||
blur_manager: BgrEffectManager::new(globals, queue_handle).ok(),
|
blur_manager: BgrEffectManager::new(globals, queue_handle).ok(),
|
||||||
|
|
||||||
|
dnd_state: Default::default(),
|
||||||
|
|
||||||
seats,
|
seats,
|
||||||
text_input_state: TextInputState::new(globals, queue_handle).ok(),
|
text_input_state: TextInputState::new(globals, queue_handle).ok(),
|
||||||
|
|
||||||
|
|||||||
@@ -243,6 +243,12 @@ impl WindowState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK: Currently to get the data device to initiate a drag-and-drop, we iterate through all
|
||||||
|
// focused seats to find one with a pointer capability. This is definitely wrong.
|
||||||
|
pub(crate) fn focused_seats(&self) -> impl Iterator<Item = &ObjectId> {
|
||||||
|
self.seat_focus.iter()
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply closure on the given pointer.
|
/// Apply closure on the given pointer.
|
||||||
fn apply_on_pointer<F: FnMut(&ThemedPointer<WinitPointerData>, &WinitPointerData)>(
|
fn apply_on_pointer<F: FnMut(&ThemedPointer<WinitPointerData>, &WinitPointerData)>(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ rwh_06.workspace = true
|
|||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
smol_str.workspace = true
|
smol_str.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
url.workspace = true
|
||||||
winit-core.workspace = true
|
winit-core.workspace = true
|
||||||
|
|
||||||
# Platform-specific
|
# Platform-specific
|
||||||
@@ -32,7 +33,9 @@ windows-sys = { workspace = true, features = [
|
|||||||
"Win32_Media",
|
"Win32_Media",
|
||||||
"Win32_System_Com_StructuredStorage",
|
"Win32_System_Com_StructuredStorage",
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
|
"Win32_System_DataExchange",
|
||||||
"Win32_System_LibraryLoader",
|
"Win32_System_LibraryLoader",
|
||||||
|
"Win32_System_Memory",
|
||||||
"Win32_System_Ole",
|
"Win32_System_Ole",
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
"Win32_System_SystemInformation",
|
"Win32_System_SystemInformation",
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
|
||||||
use windows_sys::Win32::Foundation::{HWND, POINTL};
|
use windows_sys::Win32::Foundation::{HWND, POINT, POINTL};
|
||||||
use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM};
|
use windows_sys::Win32::System::Com::{FORMATETC, STGMEDIUM};
|
||||||
|
use windows_sys::Win32::UI::Shell::SHDRAGIMAGE;
|
||||||
use windows_sys::core::{BOOL, GUID, HRESULT};
|
use windows_sys::core::{BOOL, GUID, HRESULT};
|
||||||
|
|
||||||
pub type IUnknown = *mut c_void;
|
pub type IUnknown = *mut c_void;
|
||||||
@@ -37,7 +38,7 @@ pub struct IDataObjectVtbl {
|
|||||||
pformatetc: *const FORMATETC,
|
pformatetc: *const FORMATETC,
|
||||||
pmedium: *mut STGMEDIUM,
|
pmedium: *mut STGMEDIUM,
|
||||||
) -> HRESULT,
|
) -> HRESULT,
|
||||||
QueryGetData:
|
pub QueryGetData:
|
||||||
unsafe extern "system" fn(This: *mut IDataObject, pformatetc: *const FORMATETC) -> HRESULT,
|
unsafe extern "system" fn(This: *mut IDataObject, pformatetc: *const FORMATETC) -> HRESULT,
|
||||||
pub GetCanonicalFormatEtc: unsafe extern "system" fn(
|
pub GetCanonicalFormatEtc: unsafe extern "system" fn(
|
||||||
This: *mut IDataObject,
|
This: *mut IDataObject,
|
||||||
@@ -47,7 +48,7 @@ pub struct IDataObjectVtbl {
|
|||||||
pub SetData: unsafe extern "system" fn(
|
pub SetData: unsafe extern "system" fn(
|
||||||
This: *mut IDataObject,
|
This: *mut IDataObject,
|
||||||
pformatetc: *const FORMATETC,
|
pformatetc: *const FORMATETC,
|
||||||
pformatetcOut: *const FORMATETC,
|
pmedium: *const STGMEDIUM,
|
||||||
fRelease: BOOL,
|
fRelease: BOOL,
|
||||||
) -> HRESULT,
|
) -> HRESULT,
|
||||||
pub EnumFormatEtc: unsafe extern "system" fn(
|
pub EnumFormatEtc: unsafe extern "system" fn(
|
||||||
@@ -69,6 +70,81 @@ pub struct IDataObjectVtbl {
|
|||||||
) -> HRESULT,
|
) -> HRESULT,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct IEnumFORMATETCVtbl {
|
||||||
|
pub parent: IUnknownVtbl,
|
||||||
|
pub Next: unsafe extern "system" fn(
|
||||||
|
This: *mut IEnumFORMATETC,
|
||||||
|
celt: u32,
|
||||||
|
rgelt: *mut FORMATETC,
|
||||||
|
pceltFetched: *mut u32,
|
||||||
|
) -> HRESULT,
|
||||||
|
pub Skip: unsafe extern "system" fn(This: *mut IEnumFORMATETC, celt: u32) -> HRESULT,
|
||||||
|
pub Reset: unsafe extern "system" fn(This: *mut IEnumFORMATETC) -> HRESULT,
|
||||||
|
pub Clone: unsafe extern "system" fn(
|
||||||
|
This: *mut IEnumFORMATETC,
|
||||||
|
ppenum: *mut *mut IEnumFORMATETC,
|
||||||
|
) -> HRESULT,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type IDragSourceHelper = *mut c_void;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct IDragSourceHelperVtbl {
|
||||||
|
pub parent: IUnknownVtbl,
|
||||||
|
pub InitializeFromBitmap: unsafe extern "system" fn(
|
||||||
|
This: *mut IDragSourceHelper,
|
||||||
|
pshdi: *const SHDRAGIMAGE,
|
||||||
|
pDataObject: *mut IDataObject,
|
||||||
|
) -> HRESULT,
|
||||||
|
pub InitializeFromWindow: unsafe extern "system" fn(
|
||||||
|
This: *mut IDragSourceHelper,
|
||||||
|
hwnd: HWND,
|
||||||
|
ppt: *const POINT,
|
||||||
|
pDataObject: *mut IDataObject,
|
||||||
|
) -> HRESULT,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type IDropTargetHelper = *mut c_void;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct IDropTargetHelperVtbl {
|
||||||
|
pub parent: IUnknownVtbl,
|
||||||
|
pub DragEnter: unsafe extern "system" fn(
|
||||||
|
This: *mut IDropTargetHelper,
|
||||||
|
hwndTarget: HWND,
|
||||||
|
pDataObject: *mut IDataObject,
|
||||||
|
ppt: *const POINT,
|
||||||
|
dwEffect: u32,
|
||||||
|
) -> HRESULT,
|
||||||
|
pub DragLeave: unsafe extern "system" fn(This: *mut IDropTargetHelper) -> HRESULT,
|
||||||
|
pub DragOver: unsafe extern "system" fn(
|
||||||
|
This: *mut IDropTargetHelper,
|
||||||
|
ppt: *const POINT,
|
||||||
|
dwEffect: u32,
|
||||||
|
) -> HRESULT,
|
||||||
|
pub Drop: unsafe extern "system" fn(
|
||||||
|
This: *mut IDropTargetHelper,
|
||||||
|
pDataObject: *mut IDataObject,
|
||||||
|
ppt: *const POINT,
|
||||||
|
dwEffect: u32,
|
||||||
|
) -> HRESULT,
|
||||||
|
pub Show: unsafe extern "system" fn(This: *mut IDropTargetHelper, fShow: BOOL) -> HRESULT,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type IDropSource = *mut c_void;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct IDropSourceVtbl {
|
||||||
|
pub parent: IUnknownVtbl,
|
||||||
|
pub QueryContinueDrag: unsafe extern "system" fn(
|
||||||
|
This: *mut IDropSource,
|
||||||
|
fEscapePressed: BOOL,
|
||||||
|
grfKeyState: u32,
|
||||||
|
) -> HRESULT,
|
||||||
|
pub GiveFeedback: unsafe extern "system" fn(This: *mut IDropSource, dwEffect: u32) -> HRESULT,
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct IDropTargetVtbl {
|
pub struct IDropTargetVtbl {
|
||||||
pub parent: IUnknownVtbl,
|
pub parent: IUnknownVtbl,
|
||||||
@@ -130,6 +206,22 @@ pub struct ITaskbarList2 {
|
|||||||
pub lpVtbl: *const ITaskbarList2Vtbl,
|
pub lpVtbl: *const ITaskbarList2Vtbl,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defined in `objidl.h`.
|
||||||
|
pub const IID_IDataObject: GUID = GUID::from_u128(0x0000010e_0000_0000_c000_000000000046);
|
||||||
|
|
||||||
|
/// Defined in `oleidl.h`.
|
||||||
|
pub const IID_IDropSource: GUID = GUID::from_u128(0x00000121_0000_0000_c000_000000000046);
|
||||||
|
|
||||||
|
/// Defined in `objidl.h`.
|
||||||
|
pub const IID_IEnumFORMATETC: GUID = GUID::from_u128(0x00000103_0000_0000_c000_000000000046);
|
||||||
|
|
||||||
|
/// Defined in `shobjidl_core.h`.
|
||||||
|
pub const IID_IDragSourceHelper: GUID = GUID::from_u128(0xde5bf786_477a_11d2_839d_00c04fd918d0);
|
||||||
|
|
||||||
|
/// Defined in `shobjidl_core.h`.
|
||||||
|
pub const IID_IDropTargetHelper: GUID = GUID::from_u128(0x4657278b_411b_11d2_839a_00c04fd918d0);
|
||||||
|
|
||||||
|
/// Defined in `shobjidl_core.h`.
|
||||||
pub const CLSID_TaskbarList: GUID = GUID {
|
pub const CLSID_TaskbarList: GUID = GUID {
|
||||||
data1: 0x56fdf344,
|
data1: 0x56fdf344,
|
||||||
data2: 0xfd6d,
|
data2: 0xfd6d,
|
||||||
@@ -137,6 +229,7 @@ pub const CLSID_TaskbarList: GUID = GUID {
|
|||||||
data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90],
|
data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Defined in `shobjidl_core.h`.
|
||||||
pub const IID_ITaskbarList: GUID = GUID {
|
pub const IID_ITaskbarList: GUID = GUID {
|
||||||
data1: 0x56fdf342,
|
data1: 0x56fdf342,
|
||||||
data2: 0xfd6d,
|
data2: 0xfd6d,
|
||||||
@@ -144,6 +237,7 @@ pub const IID_ITaskbarList: GUID = GUID {
|
|||||||
data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90],
|
data4: [0x95, 0x8a, 0x00, 0x60, 0x97, 0xc9, 0xa0, 0x90],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Defined in `shobjidl_core.h`.
|
||||||
pub const IID_ITaskbarList2: GUID = GUID {
|
pub const IID_ITaskbarList2: GUID = GUID {
|
||||||
data1: 0x602d4995,
|
data1: 0x602d4995,
|
||||||
data2: 0xb13a,
|
data2: 0xb13a,
|
||||||
|
|||||||
1615
winit-win32/src/dnd.rs
Normal file
1615
winit-win32/src/dnd.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,240 +0,0 @@
|
|||||||
use std::ffi::{OsString, c_void};
|
|
||||||
use std::os::windows::ffi::OsStringExt;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::ptr;
|
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
|
|
||||||
use dpi::PhysicalPosition;
|
|
||||||
use tracing::debug;
|
|
||||||
use windows_sys::Win32::Foundation::{DV_E_FORMATETC, HWND, POINT, POINTL, S_OK};
|
|
||||||
use windows_sys::Win32::Graphics::Gdi::ScreenToClient;
|
|
||||||
use windows_sys::Win32::System::Com::{DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL};
|
|
||||||
use windows_sys::Win32::System::Ole::{CF_HDROP, DROPEFFECT_COPY, DROPEFFECT_NONE};
|
|
||||||
use windows_sys::Win32::UI::Shell::{DragFinish, DragQueryFileW, HDROP};
|
|
||||||
use windows_sys::core::{GUID, HRESULT};
|
|
||||||
use winit_core::event::WindowEvent;
|
|
||||||
|
|
||||||
use crate::definitions::{
|
|
||||||
IDataObject, IDataObjectVtbl, IDropTarget, IDropTargetVtbl, IUnknown, IUnknownVtbl,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct FileDropHandlerData {
|
|
||||||
pub interface: IDropTarget,
|
|
||||||
refcount: AtomicUsize,
|
|
||||||
window: HWND,
|
|
||||||
send_event: Box<dyn Fn(WindowEvent)>,
|
|
||||||
cursor_effect: u32,
|
|
||||||
valid: bool, /* If the currently hovered item is not valid there must not be any
|
|
||||||
* `DragLeft` emitted */
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FileDropHandler {
|
|
||||||
pub data: *mut FileDropHandlerData,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
impl FileDropHandler {
|
|
||||||
pub(crate) fn new(window: HWND, send_event: Box<dyn Fn(WindowEvent)>) -> FileDropHandler {
|
|
||||||
let data = Box::new(FileDropHandlerData {
|
|
||||||
interface: IDropTarget { lpVtbl: &DROP_TARGET_VTBL as *const IDropTargetVtbl },
|
|
||||||
refcount: AtomicUsize::new(1),
|
|
||||||
window,
|
|
||||||
send_event,
|
|
||||||
cursor_effect: DROPEFFECT_NONE,
|
|
||||||
valid: false,
|
|
||||||
});
|
|
||||||
FileDropHandler { data: Box::into_raw(data) }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement IUnknown
|
|
||||||
pub unsafe extern "system" fn QueryInterface(
|
|
||||||
_this: *mut IUnknown,
|
|
||||||
_riid: *const GUID,
|
|
||||||
_ppvObject: *mut *mut c_void,
|
|
||||||
) -> HRESULT {
|
|
||||||
// This function doesn't appear to be required for an `IDropTarget`.
|
|
||||||
// An implementation would be nice however.
|
|
||||||
unimplemented!();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe extern "system" fn AddRef(this: *mut IUnknown) -> u32 {
|
|
||||||
let drop_handler_data = unsafe { Self::from_interface(this) };
|
|
||||||
let count = drop_handler_data.refcount.fetch_add(1, Ordering::Release) + 1;
|
|
||||||
count as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe extern "system" fn Release(this: *mut IUnknown) -> u32 {
|
|
||||||
let drop_handler = unsafe { Self::from_interface(this) };
|
|
||||||
let count = drop_handler.refcount.fetch_sub(1, Ordering::Release) - 1;
|
|
||||||
if count == 0 {
|
|
||||||
// Destroy the underlying data
|
|
||||||
drop(unsafe { Box::from_raw(drop_handler as *mut FileDropHandlerData) });
|
|
||||||
}
|
|
||||||
count as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe extern "system" fn DragEnter(
|
|
||||||
this: *mut IDropTarget,
|
|
||||||
pDataObj: *const IDataObject,
|
|
||||||
_grfKeyState: u32,
|
|
||||||
pt: POINTL,
|
|
||||||
pdwEffect: *mut u32,
|
|
||||||
) -> HRESULT {
|
|
||||||
let drop_handler = unsafe { Self::from_interface(this) };
|
|
||||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
|
||||||
unsafe {
|
|
||||||
ScreenToClient(drop_handler.window, &mut pt);
|
|
||||||
}
|
|
||||||
let position = PhysicalPosition::new(pt.x as f64, pt.y as f64);
|
|
||||||
let mut paths = Vec::new();
|
|
||||||
let hdrop = unsafe { Self::iterate_filenames(pDataObj, |path| paths.push(path)) };
|
|
||||||
drop_handler.valid = hdrop.is_some();
|
|
||||||
if drop_handler.valid {
|
|
||||||
(drop_handler.send_event)(WindowEvent::DragEntered { paths, position });
|
|
||||||
}
|
|
||||||
drop_handler.cursor_effect =
|
|
||||||
if drop_handler.valid { DROPEFFECT_COPY } else { DROPEFFECT_NONE };
|
|
||||||
unsafe {
|
|
||||||
*pdwEffect = drop_handler.cursor_effect;
|
|
||||||
}
|
|
||||||
|
|
||||||
S_OK
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe extern "system" fn DragOver(
|
|
||||||
this: *mut IDropTarget,
|
|
||||||
_grfKeyState: u32,
|
|
||||||
pt: POINTL,
|
|
||||||
pdwEffect: *mut u32,
|
|
||||||
) -> HRESULT {
|
|
||||||
let drop_handler = unsafe { Self::from_interface(this) };
|
|
||||||
if drop_handler.valid {
|
|
||||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
|
||||||
unsafe {
|
|
||||||
ScreenToClient(drop_handler.window, &mut pt);
|
|
||||||
}
|
|
||||||
let position = PhysicalPosition::new(pt.x as f64, pt.y as f64);
|
|
||||||
(drop_handler.send_event)(WindowEvent::DragMoved { position });
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
*pdwEffect = drop_handler.cursor_effect;
|
|
||||||
}
|
|
||||||
|
|
||||||
S_OK
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe extern "system" fn DragLeave(this: *mut IDropTarget) -> HRESULT {
|
|
||||||
let drop_handler = unsafe { Self::from_interface(this) };
|
|
||||||
if drop_handler.valid {
|
|
||||||
(drop_handler.send_event)(WindowEvent::DragLeft { position: None });
|
|
||||||
}
|
|
||||||
|
|
||||||
S_OK
|
|
||||||
}
|
|
||||||
|
|
||||||
pub unsafe extern "system" fn Drop(
|
|
||||||
this: *mut IDropTarget,
|
|
||||||
pDataObj: *const IDataObject,
|
|
||||||
_grfKeyState: u32,
|
|
||||||
pt: POINTL,
|
|
||||||
pdwEffect: *mut u32,
|
|
||||||
) -> HRESULT {
|
|
||||||
let drop_handler = unsafe { Self::from_interface(this) };
|
|
||||||
if drop_handler.valid {
|
|
||||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
|
||||||
unsafe {
|
|
||||||
ScreenToClient(drop_handler.window, &mut pt);
|
|
||||||
}
|
|
||||||
let position = PhysicalPosition::new(pt.x as f64, pt.y as f64);
|
|
||||||
let mut paths = Vec::new();
|
|
||||||
let hdrop = unsafe { Self::iterate_filenames(pDataObj, |path| paths.push(path)) };
|
|
||||||
(drop_handler.send_event)(WindowEvent::DragDropped { paths, position });
|
|
||||||
if let Some(hdrop) = hdrop {
|
|
||||||
unsafe {
|
|
||||||
DragFinish(hdrop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
*pdwEffect = drop_handler.cursor_effect;
|
|
||||||
}
|
|
||||||
|
|
||||||
S_OK
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn from_interface<'a, InterfaceT>(this: *mut InterfaceT) -> &'a mut FileDropHandlerData {
|
|
||||||
unsafe { &mut *(this as *mut _) }
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn iterate_filenames<F>(data_obj: *const IDataObject, mut callback: F) -> Option<HDROP>
|
|
||||||
where
|
|
||||||
F: FnMut(PathBuf),
|
|
||||||
{
|
|
||||||
let drop_format = FORMATETC {
|
|
||||||
cfFormat: CF_HDROP,
|
|
||||||
ptd: ptr::null_mut(),
|
|
||||||
dwAspect: DVASPECT_CONTENT,
|
|
||||||
lindex: -1,
|
|
||||||
tymed: TYMED_HGLOBAL as u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut medium = unsafe { std::mem::zeroed() };
|
|
||||||
let get_data_fn = unsafe { (*(*data_obj).cast::<IDataObjectVtbl>()).GetData };
|
|
||||||
let get_data_result = unsafe { get_data_fn(data_obj as *mut _, &drop_format, &mut medium) };
|
|
||||||
if get_data_result >= 0 {
|
|
||||||
let hdrop = unsafe { medium.u.hGlobal as HDROP };
|
|
||||||
|
|
||||||
// The second parameter (0xFFFFFFFF) instructs the function to return the item count
|
|
||||||
let item_count = unsafe { DragQueryFileW(hdrop, 0xffffffff, ptr::null_mut(), 0) };
|
|
||||||
|
|
||||||
for i in 0..item_count {
|
|
||||||
// Get the length of the path string NOT including the terminating null character.
|
|
||||||
// Previously, this was using a fixed size array of MAX_PATH length, but the
|
|
||||||
// Windows API allows longer paths under certain circumstances.
|
|
||||||
let character_count =
|
|
||||||
unsafe { DragQueryFileW(hdrop, i, ptr::null_mut(), 0) as usize };
|
|
||||||
let str_len = character_count + 1;
|
|
||||||
|
|
||||||
// Fill path_buf with the null-terminated file name
|
|
||||||
let mut path_buf = Vec::with_capacity(str_len);
|
|
||||||
unsafe {
|
|
||||||
DragQueryFileW(hdrop, i, path_buf.as_mut_ptr(), str_len as u32);
|
|
||||||
path_buf.set_len(str_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(OsString::from_wide(&path_buf[0..character_count]).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(hdrop)
|
|
||||||
} else if get_data_result == DV_E_FORMATETC {
|
|
||||||
// If the dropped item is not a file this error will occur.
|
|
||||||
// In this case it is OK to return without taking further action.
|
|
||||||
debug!("Error occurred while processing dropped/hovered item: item is not a file.");
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
debug!("Unexpected error occurred while processing dropped/hovered item.");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for FileDropHandler {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
unsafe {
|
|
||||||
FileDropHandler::Release(self.data as *mut IUnknown);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl {
|
|
||||||
parent: IUnknownVtbl {
|
|
||||||
QueryInterface: FileDropHandler::QueryInterface,
|
|
||||||
AddRef: FileDropHandler::AddRef,
|
|
||||||
Release: FileDropHandler::Release,
|
|
||||||
},
|
|
||||||
DragEnter: FileDropHandler::DragEnter,
|
|
||||||
DragOver: FileDropHandler::DragOver,
|
|
||||||
DragLeave: FileDropHandler::DragLeave,
|
|
||||||
Drop: FileDropHandler::Drop,
|
|
||||||
};
|
|
||||||
@@ -63,6 +63,9 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
|
|||||||
};
|
};
|
||||||
use winit_core::application::ApplicationHandler;
|
use winit_core::application::ApplicationHandler;
|
||||||
use winit_core::cursor::{CustomCursor, CustomCursorSource};
|
use winit_core::cursor::{CustomCursor, CustomCursorSource};
|
||||||
|
use winit_core::data_transfer::{
|
||||||
|
DataTransfer, DataTransferId, DataTransferSend, TransferType, TypedData,
|
||||||
|
};
|
||||||
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
|
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
|
||||||
use winit_core::event::{
|
use winit_core::event::{
|
||||||
DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton,
|
DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton,
|
||||||
@@ -70,8 +73,8 @@ use winit_core::event::{
|
|||||||
};
|
};
|
||||||
use winit_core::event_loop::pump_events::PumpStatus;
|
use winit_core::event_loop::pump_events::PumpStatus;
|
||||||
use winit_core::event_loop::{
|
use winit_core::event_loop::{
|
||||||
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
|
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
|
||||||
EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider,
|
DndAction, DragIcon, EventLoopProxy as RootEventLoopProxy, EventLoopProxyProvider,
|
||||||
OwnedDisplayHandle as CoreOwnedDisplayHandle,
|
OwnedDisplayHandle as CoreOwnedDisplayHandle,
|
||||||
};
|
};
|
||||||
use winit_core::keyboard::ModifiersState;
|
use winit_core::keyboard::ModifiersState;
|
||||||
@@ -82,8 +85,9 @@ pub(super) use self::runner::{Event, EventLoopRunner};
|
|||||||
use super::SelectedCursor;
|
use super::SelectedCursor;
|
||||||
use super::window::set_skip_taskbar;
|
use super::window::set_skip_taskbar;
|
||||||
use crate::dark_mode::try_theme;
|
use crate::dark_mode::try_theme;
|
||||||
|
use crate::dnd::{DropSource, FileDropHandler, SourceDataObject, WinDataTransfer, WinTypedData};
|
||||||
use crate::dpi::{become_dpi_aware, dpi_to_scale_factor};
|
use crate::dpi::{become_dpi_aware, dpi_to_scale_factor};
|
||||||
use crate::drop_handler::FileDropHandler;
|
use crate::event_loop::runner::PendingDrag;
|
||||||
use crate::icon::WinCursor;
|
use crate::icon::WinCursor;
|
||||||
use crate::ime::ImeContext;
|
use crate::ime::ImeContext;
|
||||||
use crate::keyboard::KeyEventBuilder;
|
use crate::keyboard::KeyEventBuilder;
|
||||||
@@ -478,6 +482,105 @@ impl RootActiveEventLoop for ActiveEventLoop {
|
|||||||
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
|
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fetch_data_transfer(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
type_: &dyn TransferType,
|
||||||
|
) -> Result<AsyncRequestSerial, RequestError> {
|
||||||
|
let Some(state) = self.0.drag_state(id) else {
|
||||||
|
return Err(os_error!(UnknownDataTransfer(id)).into());
|
||||||
|
};
|
||||||
|
let hint = type_.hint().ok_or(RequestError::Ignored)?;
|
||||||
|
let typed_data = WinTypedData::new(state.data.clone(), hint)
|
||||||
|
.map(|value| Arc::new(value) as Arc<dyn TypedData>)
|
||||||
|
.ok_or(RequestError::Ignored)?;
|
||||||
|
|
||||||
|
let serial = AsyncRequestSerial::get();
|
||||||
|
|
||||||
|
self.0.send_event(Event::Window {
|
||||||
|
window_id: state.window_id,
|
||||||
|
event: WindowEvent::DataTransferReceived { id, serial, value: typed_data },
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
|
||||||
|
let Some(state) = self.0.drag_state(id) else {
|
||||||
|
return Err(os_error!(UnknownDataTransfer(id)).into());
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Box::new(WinDataTransfer::new(state.data.clone())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_valid_dnd_actions(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
actions: &[DndAction],
|
||||||
|
) -> Result<(), RequestError> {
|
||||||
|
let mut state = self.0.drag_state.borrow_mut();
|
||||||
|
let Some(state) = state.as_mut().filter(|s| s.id == id) else {
|
||||||
|
return Err(os_error!(UnknownDataTransfer(id)).into());
|
||||||
|
};
|
||||||
|
state.actions = actions.to_vec();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_drag(
|
||||||
|
&self,
|
||||||
|
source: WindowId,
|
||||||
|
send_data: Box<dyn DataTransferSend>,
|
||||||
|
allowed_actions: &[DndAction],
|
||||||
|
icon: Option<DragIcon>,
|
||||||
|
) -> Result<DataTransferId, RequestError> {
|
||||||
|
let allowed_effects = crate::dnd::dnd_actions_to_dropeffect_mask(allowed_actions);
|
||||||
|
// Win32 would happily run a modal `DoDragDrop` with `allowed_effects == 0`, but every
|
||||||
|
// target would see "no action allowed" and the drag would end in a guaranteed cancel
|
||||||
|
// after burning a full modal pump. Fail fast instead - the caller asked for a drag
|
||||||
|
// they explicitly refuse to allow.
|
||||||
|
if allowed_effects == 0 {
|
||||||
|
return Err(
|
||||||
|
NotSupportedError::new("start_drag called with an empty action mask").into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = crate::dnd::next_data_transfer_id();
|
||||||
|
let data_object = SourceDataObject::new(send_data);
|
||||||
|
let drop_source = DropSource::new();
|
||||||
|
|
||||||
|
// Attach a drag preview if the app supplied one. Cosmetic failures must not abort the
|
||||||
|
// drag - the gesture still works, just without a custom image - so log and move on.
|
||||||
|
if let Some(icon) = icon {
|
||||||
|
if let Some(rgba) = icon.icon.cast_ref::<winit_core::icon::RgbaIcon>() {
|
||||||
|
let result = unsafe {
|
||||||
|
crate::dnd::apply_drag_image(
|
||||||
|
data_object.interface_ptr() as *mut _,
|
||||||
|
rgba.width(),
|
||||||
|
rgba.height(),
|
||||||
|
rgba.buffer(),
|
||||||
|
icon.offset_x,
|
||||||
|
icon.offset_y,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if let Err(hr) = result {
|
||||||
|
tracing::warn!("Failed to attach drag image: hr=0x{hr:08x}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tracing::warn!("DragIcon::icon must be an RgbaIcon on win32; ignoring");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.0.pending_drag.replace(Some(PendingDrag {
|
||||||
|
window_id: source,
|
||||||
|
id,
|
||||||
|
data_object,
|
||||||
|
drop_source,
|
||||||
|
allowed_effects,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
|
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
|
||||||
@@ -487,6 +590,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An operation was attempted on a data transfer ID, but that ID was invalid.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub struct UnknownDataTransfer(pub DataTransferId);
|
||||||
|
|
||||||
|
impl fmt::Display for UnknownDataTransfer {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let id = self.0.into_raw();
|
||||||
|
write!(f, "Unknown data transfer with ID {id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for UnknownDataTransfer {}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct OwnedDisplayHandle;
|
pub(crate) struct OwnedDisplayHandle;
|
||||||
|
|
||||||
@@ -1177,6 +1293,14 @@ unsafe fn public_window_callback_inner(
|
|||||||
result = ProcResult::Value(0);
|
result = ProcResult::Value(0);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
WM_PAINT if userdata.event_loop_runner.source_drag.get().is_some() => {
|
||||||
|
// While a source-side drag is in flight, the app handler is on the stack (we're
|
||||||
|
// inside `start_drag` -> `DoDragDrop`), so we can neither dispatch `RedrawRequested`
|
||||||
|
// nor keep re-arming via `RDW_INTERNALPAINT` (that would spin in OLE's modal loop).
|
||||||
|
// Let `DefWindowProcW` validate the region and show stale content for the duration of
|
||||||
|
// the drag; the next real paint happens once `DoDragDrop` returns.
|
||||||
|
result = ProcResult::Value(unsafe { DefWindowProcW(window, msg, wparam, lparam) });
|
||||||
|
},
|
||||||
WM_PAINT => {
|
WM_PAINT => {
|
||||||
userdata.window_state_lock().redraw_requested =
|
userdata.window_state_lock().redraw_requested =
|
||||||
userdata.event_loop_runner.should_buffer();
|
userdata.event_loop_runner.should_buffer();
|
||||||
@@ -2352,6 +2476,13 @@ unsafe fn public_window_callback_inner(
|
|||||||
.catch_unwind(callback)
|
.catch_unwind(callback)
|
||||||
.unwrap_or_else(|| result = ProcResult::Value(-1));
|
.unwrap_or_else(|| result = ProcResult::Value(-1));
|
||||||
|
|
||||||
|
// We execute a new drag operation here instead of immediately starting it in
|
||||||
|
// `ActiveEventLoop::start_drag`. `DoDragDrop` is blocking and synchronous, so if we started
|
||||||
|
// it inside the event loop then an internal drag operation would be re-entrant and the
|
||||||
|
// application would not be able to handle the incoming messages. This is after the application
|
||||||
|
// has had a chance to handle mouse events.
|
||||||
|
userdata.event_loop_runner.try_execute_drag_drop();
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
ProcResult::DefWindowProc(wparam) => unsafe { DefWindowProcW(window, msg, wparam, lparam) },
|
ProcResult::DefWindowProc(wparam) => unsafe { DefWindowProcW(window, msg, wparam, lparam) },
|
||||||
ProcResult::Value(val) => val,
|
ProcResult::Value(val) => val,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, Ref, RefCell};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@@ -7,18 +7,49 @@ use std::time::Instant;
|
|||||||
use std::{fmt, mem, panic};
|
use std::{fmt, mem, panic};
|
||||||
|
|
||||||
use dpi::PhysicalSize;
|
use dpi::PhysicalSize;
|
||||||
use windows_sys::Win32::Foundation::HWND;
|
use windows_sys::Win32::Foundation::{DRAGDROP_S_CANCEL, DRAGDROP_S_DROP, HWND};
|
||||||
|
use windows_sys::Win32::System::Ole::{
|
||||||
|
DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE, DROPEFFECT_NONE, DoDragDrop,
|
||||||
|
};
|
||||||
use winit_core::application::ApplicationHandler;
|
use winit_core::application::ApplicationHandler;
|
||||||
|
use winit_core::data_transfer::DataTransferId;
|
||||||
use winit_core::event::{DeviceEvent, DeviceId, StartCause, SurfaceSizeWriter, WindowEvent};
|
use winit_core::event::{DeviceEvent, DeviceId, StartCause, SurfaceSizeWriter, WindowEvent};
|
||||||
use winit_core::event_loop::ActiveEventLoop as RootActiveEventLoop;
|
use winit_core::event_loop::{ActiveEventLoop as RootActiveEventLoop, DndAction};
|
||||||
use winit_core::window::WindowId;
|
use winit_core::window::WindowId;
|
||||||
|
|
||||||
use super::{ActiveEventLoop, ControlFlow, EventLoopThreadExecutor};
|
use super::{ActiveEventLoop, ControlFlow, EventLoopThreadExecutor};
|
||||||
|
use crate::dnd::{DataObject, DropEffect, DropSource, SourceDataObject, drop_effect_to_dnd_action};
|
||||||
use crate::event_loop::{GWL_USERDATA, WindowData};
|
use crate::event_loop::{GWL_USERDATA, WindowData};
|
||||||
use crate::util::get_window_long;
|
use crate::util::get_window_long;
|
||||||
|
|
||||||
type EventHandler = Cell<Option<&'static mut (dyn ApplicationHandler + 'static)>>;
|
type EventHandler = Cell<Option<&'static mut (dyn ApplicationHandler + 'static)>>;
|
||||||
|
|
||||||
|
/// State for the single drag-and-drop transfer currently in flight (OLE guarantees at most one
|
||||||
|
/// active drag per process).
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) struct DragState {
|
||||||
|
pub(super) id: DataTransferId,
|
||||||
|
pub(super) window_id: WindowId,
|
||||||
|
pub(super) data: Arc<DataObject>,
|
||||||
|
pub(super) actions: Vec<DndAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct PendingDrag {
|
||||||
|
pub(super) window_id: WindowId,
|
||||||
|
pub(super) data_object: SourceDataObject,
|
||||||
|
pub(super) drop_source: DropSource,
|
||||||
|
pub(super) allowed_effects: DropEffect,
|
||||||
|
pub(super) id: DataTransferId,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set while `DoDragDrop` is on the call stack - i.e., this process is the source of an active
|
||||||
|
/// drag. The target-side `IDropTarget` checks this to recognize self-drops and reuse the source's
|
||||||
|
/// id + allowed actions instead of waiting for the (buffered) app `DragEntered` handler.
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub(crate) struct SourceDrag {
|
||||||
|
pub(crate) id: DataTransferId,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct EventLoopRunner {
|
pub(crate) struct EventLoopRunner {
|
||||||
pub(super) thread_id: u32,
|
pub(super) thread_id: u32,
|
||||||
|
|
||||||
@@ -37,6 +68,29 @@ pub(crate) struct EventLoopRunner {
|
|||||||
event_handler: Rc<EventHandler>,
|
event_handler: Rc<EventHandler>,
|
||||||
event_buffer: RefCell<VecDeque<Event>>,
|
event_buffer: RefCell<VecDeque<Event>>,
|
||||||
|
|
||||||
|
/// The currently in-flight drag transfer, if any, alive between `DragEntered` and
|
||||||
|
/// `DragLeft`/`DragDropped`.
|
||||||
|
pub(super) drag_state: RefCell<Option<DragState>>,
|
||||||
|
|
||||||
|
/// `Some(_)` while `start_drag` has `DoDragDrop` on the call stack.
|
||||||
|
pub(crate) source_drag: Cell<Option<SourceDrag>>,
|
||||||
|
|
||||||
|
/// `DoDragDrop` is blocking and synchronous, so we wait until after the application returns
|
||||||
|
/// control to winit before actually calling into the OS to initiate the drag. This prevents
|
||||||
|
/// the event loop from being re-entrant if we are doing an internal drag operation, since if
|
||||||
|
/// we handled this inside `ActiveEventLoop::start_drag` then all the `WindowEvent::Drag*`
|
||||||
|
/// events would be buffered until `DoDragDrop` returns, preventing the application from
|
||||||
|
/// handling those messages.
|
||||||
|
pub(super) pending_drag: RefCell<Option<PendingDrag>>,
|
||||||
|
|
||||||
|
/// For self-drops, target-side `IDropTarget::Drop` can't release the cached `DragState`
|
||||||
|
/// before its `DragDropped` `WindowEvent` is delivered - the event is buffered (the outer app
|
||||||
|
/// handler holds `event_handler` for the duration of `DoDragDrop`) and `data_transfer(id)`
|
||||||
|
/// would return `UnknownDataTransfer` if cleanup ran synchronously. So we stash the id here
|
||||||
|
/// and drain it at the end of `dispatch_buffered_events`, after the app's buffered handler
|
||||||
|
/// has had its chance to read the data.
|
||||||
|
pending_source_drag_cleanup: Cell<Option<DataTransferId>>,
|
||||||
|
|
||||||
panic_error: Cell<Option<PanicError>>,
|
panic_error: Cell<Option<PanicError>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +141,122 @@ impl EventLoopRunner {
|
|||||||
last_events_cleared: Cell::new(Instant::now()),
|
last_events_cleared: Cell::new(Instant::now()),
|
||||||
event_handler: Rc::new(Cell::new(None)),
|
event_handler: Rc::new(Cell::new(None)),
|
||||||
event_buffer: RefCell::new(VecDeque::new()),
|
event_buffer: RefCell::new(VecDeque::new()),
|
||||||
|
drag_state: RefCell::new(None),
|
||||||
|
source_drag: Cell::new(None),
|
||||||
|
pending_drag: RefCell::new(None),
|
||||||
|
pending_source_drag_cleanup: Cell::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn try_execute_drag_drop(self: &Rc<Self>) {
|
||||||
|
let Some(PendingDrag { data_object, drop_source, id, allowed_effects, window_id }) =
|
||||||
|
self.pending_drag.take()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make the drag visible to our own target-side `IDropTarget` so it can recognize
|
||||||
|
// self-drops and reuse this id + action mask without going through the (buffered) app
|
||||||
|
// handler. The guard ensures the flag is cleared on any exit path - if anything between
|
||||||
|
// here and `DoDragDrop`'s return panics, the stale flag would otherwise permanently
|
||||||
|
// disable `WM_PAINT` dispatch and misclassify all future external drags as self-drops.
|
||||||
|
struct ClearOnDrop<'a>(&'a Cell<Option<SourceDrag>>);
|
||||||
|
impl Drop for ClearOnDrop<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.set(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.source_drag.set(Some(SourceDrag { id }));
|
||||||
|
let _guard = ClearOnDrop(&self.source_drag);
|
||||||
|
|
||||||
|
let mut effect_out: u32 = DROPEFFECT_NONE;
|
||||||
|
let hr = unsafe {
|
||||||
|
DoDragDrop(
|
||||||
|
data_object.interface_ptr(),
|
||||||
|
drop_source.interface_ptr(),
|
||||||
|
allowed_effects,
|
||||||
|
&mut effect_out,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if hr == DRAGDROP_S_DROP {
|
||||||
|
let action = drop_effect_to_dnd_action(effect_out);
|
||||||
|
|
||||||
|
self.send_event(Event::Window {
|
||||||
|
window_id,
|
||||||
|
event: WindowEvent::OutgoingDragDropped { id, action },
|
||||||
|
});
|
||||||
|
} else if hr == DRAGDROP_S_CANCEL {
|
||||||
|
self.send_event(Event::Window {
|
||||||
|
window_id,
|
||||||
|
event: WindowEvent::OutgoingDragCanceled { id },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tracing::error!("DoDragDrop failed: 0x{hr:08x}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both `DRAGDROP_S_DROP` and `DRAGDROP_S_CANCEL` are success codes for us - the app
|
||||||
|
// will hear about the outcome via the buffered `DragDropped`/`DragLeft` events
|
||||||
|
// (target-side translates `effect_out == DROPEFFECT_NONE` to `DragLeft`).
|
||||||
|
// Log the negotiated effect so cross-process drops, which have no target-side event
|
||||||
|
// in this process, leave a debuggable trace of what action the remote target performed.
|
||||||
|
tracing::trace!(
|
||||||
|
"DoDragDrop completed: hr=0x{hr:08x} effect_out={effect_out} (COPY={DROPEFFECT_COPY}, \
|
||||||
|
MOVE={DROPEFFECT_MOVE}, LINK={DROPEFFECT_LINK})",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn defer_source_drag_cleanup(&self, id: DataTransferId) {
|
||||||
|
self.pending_source_drag_cleanup.set(Some(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn register_data_transfer(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
window_id: WindowId,
|
||||||
|
data: Arc<DataObject>,
|
||||||
|
) {
|
||||||
|
// By default, no actions have been set as valid by the target.
|
||||||
|
*self.drag_state.borrow_mut() =
|
||||||
|
Some(DragState { id, window_id, data, actions: Default::default() });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_data_transfer(&self, id: DataTransferId) {
|
||||||
|
let mut state = self.drag_state.borrow_mut();
|
||||||
|
if state.as_ref().is_some_and(|s| s.id == id) {
|
||||||
|
*state = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn drag_state(&self, id: DataTransferId) -> Option<Ref<'_, DragState>> {
|
||||||
|
Ref::filter_map(self.drag_state.borrow(), |state| state.as_ref().filter(|s| s.id == id))
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn current_drag_actions(&self, id: DataTransferId) -> Ref<'_, [DndAction]> {
|
||||||
|
Ref::map(self.drag_state.borrow(), |state| {
|
||||||
|
state.as_ref().filter(|s| s.id == id).map(|s| &s.actions[..]).unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn proposed_dnd_action(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
effects: DropEffect,
|
||||||
|
) -> Option<DndAction> {
|
||||||
|
self.current_drag_actions(id).iter().copied().find(|action| {
|
||||||
|
let effect = match action {
|
||||||
|
DndAction::Move => DROPEFFECT_MOVE,
|
||||||
|
DndAction::Copy => DROPEFFECT_COPY,
|
||||||
|
DndAction::Link => DROPEFFECT_LINK,
|
||||||
|
_ => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
(effects & effect) != 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Associate the application's event handler with the runner.
|
/// Associate the application's event handler with the runner.
|
||||||
///
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
@@ -138,12 +305,20 @@ impl EventLoopRunner {
|
|||||||
last_events_cleared: _,
|
last_events_cleared: _,
|
||||||
event_handler,
|
event_handler,
|
||||||
event_buffer: _,
|
event_buffer: _,
|
||||||
|
drag_state,
|
||||||
|
source_drag,
|
||||||
|
pending_drag,
|
||||||
|
pending_source_drag_cleanup,
|
||||||
} = self;
|
} = self;
|
||||||
interrupt_msg_dispatch.set(false);
|
interrupt_msg_dispatch.set(false);
|
||||||
runner_state.set(RunnerState::Uninitialized);
|
runner_state.set(RunnerState::Uninitialized);
|
||||||
panic_error.set(None);
|
panic_error.set(None);
|
||||||
exit.set(None);
|
exit.set(None);
|
||||||
event_handler.set(None);
|
event_handler.set(None);
|
||||||
|
drag_state.take();
|
||||||
|
source_drag.set(None);
|
||||||
|
pending_drag.take();
|
||||||
|
pending_source_drag_cleanup.set(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,6 +460,11 @@ impl EventLoopRunner {
|
|||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The app's buffered `DragDropped` handler (if any) has now had its chance to call
|
||||||
|
// `data_transfer(id)`; safe to release the cached `DragState` for a deferred self-drop.
|
||||||
|
if let Some(id) = self.pending_source_drag_cleanup.take() {
|
||||||
|
self.remove_data_transfer(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispatch control flow events (`NewEvents`, `AboutToWait`, and
|
/// Dispatch control flow events (`NewEvents`, `AboutToWait`, and
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
mod util;
|
mod util;
|
||||||
mod dark_mode;
|
mod dark_mode;
|
||||||
mod definitions;
|
mod definitions;
|
||||||
|
mod dnd;
|
||||||
mod dpi;
|
mod dpi;
|
||||||
mod drop_handler;
|
|
||||||
mod event_loop;
|
mod event_loop;
|
||||||
mod icon;
|
mod icon;
|
||||||
mod ime;
|
mod ime;
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ use crate::dark_mode::try_theme;
|
|||||||
use crate::definitions::{
|
use crate::definitions::{
|
||||||
CLSID_TaskbarList, IID_ITaskbarList, IID_ITaskbarList2, ITaskbarList, ITaskbarList2,
|
CLSID_TaskbarList, IID_ITaskbarList, IID_ITaskbarList2, ITaskbarList, ITaskbarList2,
|
||||||
};
|
};
|
||||||
|
use crate::dnd::FileDropHandler;
|
||||||
use crate::dpi::{dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi};
|
use crate::dpi::{dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi};
|
||||||
use crate::drop_handler::FileDropHandler;
|
|
||||||
use crate::event_loop::{self, ActiveEventLoop, DESTROY_MSG_ID, Event, EventLoopRunner};
|
use crate::event_loop::{self, ActiveEventLoop, DESTROY_MSG_ID, Event, EventLoopRunner};
|
||||||
use crate::icon::{IconType, WinCursor};
|
use crate::icon::{IconType, WinCursor};
|
||||||
use crate::ime::ImeContext;
|
use crate::ime::ImeContext;
|
||||||
@@ -1191,6 +1191,7 @@ impl InitData<'_> {
|
|||||||
let window_state = {
|
let window_state = {
|
||||||
let window_state = WindowState::new(
|
let window_state = WindowState::new(
|
||||||
&self.attributes,
|
&self.attributes,
|
||||||
|
&self.win_attributes,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
current_theme,
|
current_theme,
|
||||||
self.attributes.preferred_theme,
|
self.attributes.preferred_theme,
|
||||||
@@ -1230,15 +1231,16 @@ impl InitData<'_> {
|
|||||||
|
|
||||||
let file_drop_runner = self.runner.clone();
|
let file_drop_runner = self.runner.clone();
|
||||||
let window_id = win.id();
|
let window_id = win.id();
|
||||||
let file_drop_handler = FileDropHandler::new(
|
let mut file_drop_handler = FileDropHandler::new(
|
||||||
win.window.hwnd(),
|
win.window.hwnd(),
|
||||||
|
self.runner.clone(),
|
||||||
Box::new(move |event| {
|
Box::new(move |event| {
|
||||||
file_drop_runner.send_event(Event::Window { window_id, event })
|
file_drop_runner.send_event(Event::Window { window_id, event })
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let handler_interface_ptr =
|
let handler_interface_ptr =
|
||||||
unsafe { &mut (*file_drop_handler.data).interface as *mut _ as *mut c_void };
|
unsafe { file_drop_handler.interface_unchecked_mut() as *mut _ as *mut c_void };
|
||||||
|
|
||||||
assert_eq!(unsafe { RegisterDragDrop(win.window.hwnd(), handler_interface_ptr) }, S_OK);
|
assert_eq!(unsafe { RegisterDragDrop(win.window.hwnd(), handler_interface_ptr) }, S_OK);
|
||||||
Some(file_drop_handler)
|
Some(file_drop_handler)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use winit_core::keyboard::ModifiersState;
|
|||||||
use winit_core::monitor::Fullscreen;
|
use winit_core::monitor::Fullscreen;
|
||||||
use winit_core::window::{ImeCapabilities, Theme, WindowAttributes};
|
use winit_core::window::{ImeCapabilities, Theme, WindowAttributes};
|
||||||
|
|
||||||
use crate::{SelectedCursor, event_loop, util};
|
use crate::{SelectedCursor, WindowAttributesWindows, event_loop, util};
|
||||||
|
|
||||||
/// Contains information about states and the window that the callback is going to use.
|
/// Contains information about states and the window that the callback is going to use.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -154,6 +154,7 @@ pub enum ImeState {
|
|||||||
impl WindowState {
|
impl WindowState {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
attributes: &WindowAttributes,
|
attributes: &WindowAttributes,
|
||||||
|
_win_attributes: &WindowAttributesWindows,
|
||||||
scale_factor: f64,
|
scale_factor: f64,
|
||||||
current_theme: Theme,
|
current_theme: Theme,
|
||||||
preferred_theme: Option<Theme>,
|
preferred_theme: Option<Theme>,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ macro_rules! atom_manager {
|
|||||||
|
|
||||||
/// Indices into the `Atoms` struct.
|
/// Indices into the `Atoms` struct.
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
|
||||||
pub enum AtomName {
|
pub enum AtomName {
|
||||||
$($name,)*
|
$($name,)*
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,7 @@ macro_rules! atom_manager {
|
|||||||
atom_manager! {
|
atom_manager! {
|
||||||
// General Use Atoms
|
// General Use Atoms
|
||||||
CARD32,
|
CARD32,
|
||||||
|
STRING,
|
||||||
UTF8_STRING,
|
UTF8_STRING,
|
||||||
WM_CHANGE_STATE,
|
WM_CHANGE_STATE,
|
||||||
WM_CLIENT_MACHINE,
|
WM_CLIENT_MACHINE,
|
||||||
@@ -87,11 +88,39 @@ atom_manager! {
|
|||||||
XdndDrop,
|
XdndDrop,
|
||||||
XdndPosition,
|
XdndPosition,
|
||||||
XdndStatus,
|
XdndStatus,
|
||||||
XdndActionPrivate,
|
XdndActionCopy,
|
||||||
XdndSelection,
|
XdndSelection,
|
||||||
XdndFinished,
|
XdndFinished,
|
||||||
XdndTypeList,
|
XdndTypeList,
|
||||||
|
|
||||||
|
// MIME types for reading selections
|
||||||
TextUriList: b"text/uri-list",
|
TextUriList: b"text/uri-list",
|
||||||
|
TextPlain: b"text/plain",
|
||||||
|
TextPlainCharsetUtf8: b"text/plain;charset=utf-8",
|
||||||
|
TextHtml: b"text/html",
|
||||||
|
TextHtmlCharsetUtf8: b"text/html;charset=utf-8",
|
||||||
|
ApplicationRtf: b"application/rtf",
|
||||||
|
AudioAac: b"audio/aac",
|
||||||
|
AudioAiff: b"audio/aiff",
|
||||||
|
AudioFlac: b"audio/flac",
|
||||||
|
AudioWav: b"audio/wav",
|
||||||
|
AudioWave: b"audio/wave",
|
||||||
|
AudioXWav: b"audio/x-wav",
|
||||||
|
AudioVndWav: b"audio/vnd.wav",
|
||||||
|
AudioVndWave: b"audio/vnd.wave",
|
||||||
|
AudioMpeg: b"audio/mpeg",
|
||||||
|
AudioOgg: b"audio/ogg",
|
||||||
|
ImageBmp: b"image/bmp",
|
||||||
|
ImageGif: b"image/gif",
|
||||||
|
ImageJpeg: b"image/jpeg",
|
||||||
|
ImagePjpeg: b"image/pjpeg",
|
||||||
|
ImagePng: b"image/png",
|
||||||
|
ImageSvg: b"image/svg+xml",
|
||||||
|
ImageTiff: b"image/tiff",
|
||||||
|
ImageWebp: b"image/webp",
|
||||||
|
ImageXIcon: b"image/x-icon",
|
||||||
|
ImageRaw: b"image/x-panasonic-raw",
|
||||||
|
|
||||||
None: b"None",
|
None: b"None",
|
||||||
|
|
||||||
// Miscellaneous Atoms
|
// Miscellaneous Atoms
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
|
use std::collections::VecDeque;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::raw::*;
|
use std::os::raw::*;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::str::Utf8Error;
|
use std::str::Utf8Error;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicI64, Ordering};
|
||||||
|
|
||||||
use dpi::PhysicalPosition;
|
use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType, TypeHint, TypedData};
|
||||||
use percent_encoding::percent_decode;
|
use winit_core::event_loop::AsyncRequestSerial;
|
||||||
use x11rb::protocol::xproto::{self, ConnectionExt};
|
use x11rb::protocol::xproto::{self, ConnectionExt};
|
||||||
|
|
||||||
use crate::atoms::AtomName::None as DndNone;
|
use crate::atoms::AtomName::None as DndNone;
|
||||||
use crate::atoms::*;
|
use crate::atoms::*;
|
||||||
use crate::event_loop::{CookieResultExt, X11Error};
|
use crate::event_loop::{CookieResultExt, X11Error};
|
||||||
use crate::util;
|
|
||||||
use crate::xdisplay::XConnection;
|
use crate::xdisplay::XConnection;
|
||||||
|
use crate::{XWindow, util};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum DndState {
|
pub enum DndState {
|
||||||
@@ -21,98 +22,264 @@ pub enum DndState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum DndDataParseError {
|
pub enum UriListParseError {
|
||||||
EmptyData,
|
EmptyData,
|
||||||
InvalidUtf8(#[allow(dead_code)] Utf8Error),
|
InvalidUtf8(#[allow(dead_code)] Utf8Error),
|
||||||
HostnameSpecified(#[allow(dead_code)] String),
|
HostnameSpecified(#[allow(dead_code)] String),
|
||||||
UnexpectedProtocol(#[allow(dead_code)] String),
|
UnexpectedProtocol(#[allow(dead_code)] String),
|
||||||
UnresolvablePath(#[allow(dead_code)] io::Error),
|
UnresolvablePath(#[allow(dead_code)] io::Error),
|
||||||
|
Io(#[allow(dead_code)] io::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Utf8Error> for DndDataParseError {
|
impl From<Utf8Error> for UriListParseError {
|
||||||
fn from(e: Utf8Error) -> Self {
|
fn from(e: Utf8Error) -> Self {
|
||||||
DndDataParseError::InvalidUtf8(e)
|
UriListParseError::InvalidUtf8(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<io::Error> for DndDataParseError {
|
impl From<io::Error> for UriListParseError {
|
||||||
fn from(e: io::Error) -> Self {
|
fn from(e: io::Error) -> Self {
|
||||||
DndDataParseError::UnresolvablePath(e)
|
UriListParseError::UnresolvablePath(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SelectionReader {
|
||||||
|
type_: SelectionType,
|
||||||
|
data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypedData for SelectionReader {
|
||||||
|
fn try_read(&self) -> Option<Box<dyn io::BufRead>> {
|
||||||
|
Some(Box::new(io::Cursor::new(self.data.clone())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_(&self) -> &dyn TransferType {
|
||||||
|
&self.type_
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_as_string(&self) -> io::Result<String> {
|
||||||
|
fn invalid_data<E>(err: E) -> io::Error
|
||||||
|
where
|
||||||
|
E: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||||
|
{
|
||||||
|
io::Error::new(io::ErrorKind::InvalidData, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_utf16_bytes(bytes: &[u8]) -> io::Result<String> {
|
||||||
|
let utf16 = bytes
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|chunk| {
|
||||||
|
let bytes: &[u8; 2] = chunk.try_into().unwrap();
|
||||||
|
u16::from_ne_bytes(*bytes)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
String::from_utf16(&utf16).map_err(invalid_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.type_.hint() {
|
||||||
|
Some(TypeHint::Plaintext) | Some(TypeHint::Html) => std::str::from_utf8(&self.data)
|
||||||
|
.map(|str| str.to_owned())
|
||||||
|
.map_err(invalid_data)
|
||||||
|
.or_else(|_| decode_utf16_bytes(&self.data)),
|
||||||
|
Some(TypeHint::UriList) => String::from_utf8(self.data.clone()).map_err(invalid_data),
|
||||||
|
_ => Err(io::ErrorKind::InvalidData.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_as_uris(&self) -> io::Result<Vec<String>> {
|
||||||
|
if self.type_().hint() != Some(TypeHint::UriList) {
|
||||||
|
return Err(io::ErrorKind::InvalidData.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self
|
||||||
|
.try_as_string()?
|
||||||
|
.split(['\n', '\r'])
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DragState {
|
||||||
|
// Populated by XdndEnter event handler
|
||||||
|
pub version: c_long,
|
||||||
|
pub transfer_id: DataTransferId,
|
||||||
|
pub types: Arc<[SelectionType]>,
|
||||||
|
// Populated by Xdnd* event handlers
|
||||||
|
pub source_window: xproto::Window,
|
||||||
|
// Populated by Xdnd* event handlers
|
||||||
|
pub target_window: xproto::Window,
|
||||||
|
// Populated by `fetch_data_transfer`
|
||||||
|
pub pending_fetch_types: VecDeque<(AsyncRequestSerial, SelectionType)>,
|
||||||
|
pub finished: Option<(XWindow, XWindow)>,
|
||||||
|
/// Whether the drag operation is accepted (or `None` if the user never indicated that it's
|
||||||
|
/// accepted or rejected)
|
||||||
|
// Populated by `Window::accept_drag`/`Window::reject_drag`.
|
||||||
|
pub accepted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DragState {
|
||||||
|
fn default() -> Self {
|
||||||
|
static DATA_TRANSFER_ID: AtomicI64 = AtomicI64::new(0);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
version: Default::default(),
|
||||||
|
transfer_id: DataTransferId::from_raw(DATA_TRANSFER_ID.fetch_add(1, Ordering::Relaxed)),
|
||||||
|
types: Default::default(),
|
||||||
|
source_window: Default::default(),
|
||||||
|
target_window: Default::default(),
|
||||||
|
pending_fetch_types: Default::default(),
|
||||||
|
finished: None,
|
||||||
|
accepted: Default::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Dnd {
|
pub struct Dnd {
|
||||||
xconn: Arc<XConnection>,
|
xconn: Arc<XConnection>,
|
||||||
// Populated by XdndEnter event handler
|
// If `None`, no drag operation is in progress.
|
||||||
pub version: Option<c_long>,
|
state: Option<DragState>,
|
||||||
pub type_list: Option<Vec<xproto::Atom>>,
|
}
|
||||||
// Populated by XdndPosition event handler
|
|
||||||
pub source_window: Option<xproto::Window>,
|
#[derive(Debug)]
|
||||||
// Populated by XdndPosition event handler
|
pub struct Selection {
|
||||||
pub position: PhysicalPosition<f64>,
|
types: Arc<[SelectionType]>,
|
||||||
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
|
}
|
||||||
pub result: Option<Result<Vec<PathBuf>, DndDataParseError>>,
|
|
||||||
// Populated by SelectionNotify event handler (triggered by XdndPosition event handler)
|
impl Selection {
|
||||||
pub dragging: bool,
|
pub(crate) fn new(types: Arc<[SelectionType]>) -> Selection {
|
||||||
|
Selection { types }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct SelectionType {
|
||||||
|
hint: Option<TypeHint>,
|
||||||
|
atom: xproto::Atom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectionType {
|
||||||
|
pub(crate) fn new(atoms: &Atoms, atom: xproto::Atom) -> Self {
|
||||||
|
let atom_to_hint = [
|
||||||
|
// Files
|
||||||
|
(atoms[TextUriList], TypeHint::UriList),
|
||||||
|
// Plaintext
|
||||||
|
(atoms[STRING], TypeHint::Plaintext),
|
||||||
|
(atoms[UTF8_STRING], TypeHint::Plaintext),
|
||||||
|
(atoms[TextPlain], TypeHint::Plaintext),
|
||||||
|
(atoms[TextPlainCharsetUtf8], TypeHint::Plaintext),
|
||||||
|
// HTML
|
||||||
|
(atoms[TextHtml], TypeHint::Html),
|
||||||
|
(atoms[TextHtmlCharsetUtf8], TypeHint::Html),
|
||||||
|
// RTF
|
||||||
|
(atoms[ApplicationRtf], TypeHint::Rtf),
|
||||||
|
// Audio
|
||||||
|
(atoms[AudioAac], TypeHint::Audio { extension_hint: Some("aac") }),
|
||||||
|
(atoms[AudioAiff], TypeHint::Audio { extension_hint: Some("aif") }),
|
||||||
|
(atoms[AudioFlac], TypeHint::Audio { extension_hint: Some("flac") }),
|
||||||
|
(atoms[AudioVndWav], TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(atoms[AudioVndWave], TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(atoms[AudioWav], TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(atoms[AudioWave], TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(atoms[AudioXWav], TypeHint::Audio { extension_hint: Some("wav") }),
|
||||||
|
(atoms[AudioOgg], TypeHint::Audio { extension_hint: Some("ogg") }),
|
||||||
|
(atoms[AudioMpeg], TypeHint::Audio { extension_hint: Some("mp3") }),
|
||||||
|
// Image
|
||||||
|
(atoms[ImageBmp], TypeHint::Image { extension_hint: Some("bmp") }),
|
||||||
|
(atoms[ImageGif], TypeHint::Image { extension_hint: Some("gif") }),
|
||||||
|
(atoms[ImageJpeg], TypeHint::Image { extension_hint: Some("jpg") }),
|
||||||
|
(atoms[ImagePjpeg], TypeHint::Image { extension_hint: Some("jpg") }),
|
||||||
|
(atoms[ImagePng], TypeHint::Image { extension_hint: Some("png") }),
|
||||||
|
(atoms[ImageRaw], TypeHint::Image { extension_hint: Some("raw") }),
|
||||||
|
(atoms[ImageSvg], TypeHint::Image { extension_hint: Some("svg") }),
|
||||||
|
(atoms[ImageTiff], TypeHint::Image { extension_hint: Some("tiff") }),
|
||||||
|
(atoms[ImageWebp], TypeHint::Image { extension_hint: Some("webp") }),
|
||||||
|
(atoms[ImageXIcon], TypeHint::Image { extension_hint: Some("ico") }),
|
||||||
|
];
|
||||||
|
let hint =
|
||||||
|
atom_to_hint.iter().find_map(|(haystack, hint)| (*haystack == atom).then_some(*hint));
|
||||||
|
|
||||||
|
Self { hint, atom }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn atom(&self) -> xproto::Atom {
|
||||||
|
self.atom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransferType for SelectionType {
|
||||||
|
fn hint(&self) -> Option<TypeHint> {
|
||||||
|
self.hint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, other: &dyn TransferType) -> bool {
|
||||||
|
if let Some(other_mime) = other.cast_ref::<Self>() {
|
||||||
|
*self == *other_mime
|
||||||
|
} else {
|
||||||
|
// If either hint is `None`, return false
|
||||||
|
self.hint().is_some_and(|hint| other.hint() == Some(hint))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataTransfer for Selection {
|
||||||
|
fn for_each_available_type<'this>(
|
||||||
|
&'this self,
|
||||||
|
func: &'_ mut dyn FnMut(&'this dyn TransferType) -> std::ops::ControlFlow<()>,
|
||||||
|
) {
|
||||||
|
let _ = self.types.iter().map(|mime| mime as &dyn TransferType).try_for_each(func);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dnd {
|
impl Dnd {
|
||||||
pub fn new(xconn: Arc<XConnection>) -> Result<Self, X11Error> {
|
pub fn new(xconn: Arc<XConnection>) -> Self {
|
||||||
Ok(Dnd {
|
Dnd { xconn, state: None }
|
||||||
xconn,
|
|
||||||
version: None,
|
|
||||||
type_list: None,
|
|
||||||
source_window: None,
|
|
||||||
position: PhysicalPosition::default(),
|
|
||||||
result: None,
|
|
||||||
dragging: false,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn state(&self) -> Option<&DragState> {
|
||||||
self.version = None;
|
self.state.as_ref()
|
||||||
self.type_list = None;
|
|
||||||
self.source_window = None;
|
|
||||||
self.result = None;
|
|
||||||
self.dragging = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn send_status(
|
pub fn state_mut(&mut self) -> Option<&mut DragState> {
|
||||||
&self,
|
self.state.as_mut()
|
||||||
this_window: xproto::Window,
|
}
|
||||||
|
|
||||||
|
pub fn find_type_by_hint(&self, hint: TypeHint) -> Option<&SelectionType> {
|
||||||
|
self.state.as_ref()?.types.iter().find(|haystack| haystack.hint() == Some(hint))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_state(
|
||||||
|
&mut self,
|
||||||
|
version: c_long,
|
||||||
|
source_window: xproto::Window,
|
||||||
target_window: xproto::Window,
|
target_window: xproto::Window,
|
||||||
state: DndState,
|
types: Arc<[SelectionType]>,
|
||||||
) -> Result<(), X11Error> {
|
) -> &DragState {
|
||||||
let atoms = self.xconn.atoms();
|
self.state.get_or_insert(DragState {
|
||||||
let (accepted, action) = match state {
|
version,
|
||||||
DndState::Accepted => (1, atoms[XdndActionPrivate]),
|
types,
|
||||||
DndState::Rejected => (0, atoms[DndNone]),
|
source_window,
|
||||||
};
|
target_window,
|
||||||
self.xconn
|
..Default::default()
|
||||||
.send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [
|
})
|
||||||
this_window,
|
|
||||||
accepted,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
action as _,
|
|
||||||
])?
|
|
||||||
.ignore_error();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn send_finished(
|
pub unsafe fn send_finished(
|
||||||
&self,
|
&self,
|
||||||
this_window: xproto::Window,
|
this_window: xproto::Window,
|
||||||
target_window: xproto::Window,
|
target_window: xproto::Window,
|
||||||
state: DndState,
|
|
||||||
) -> Result<(), X11Error> {
|
) -> Result<(), X11Error> {
|
||||||
let atoms = self.xconn.atoms();
|
let atoms = self.xconn.atoms();
|
||||||
let (accepted, action) = match state {
|
let Some(state) = &self.state else {
|
||||||
DndState::Accepted => (1, atoms[XdndActionPrivate]),
|
return Err(X11Error::UnexpectedNull(
|
||||||
DndState::Rejected => (0, atoms[DndNone]),
|
"Drag-and-drop state was not initialized (called `send_finished` before XdndEnter",
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
let (accepted, action) =
|
||||||
|
if state.accepted { (1, atoms[XdndActionCopy]) } else { (0, atoms[DndNone]) };
|
||||||
self.xconn
|
self.xconn
|
||||||
.send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [
|
.send_client_msg(target_window, target_window, atoms[XdndFinished] as _, None, [
|
||||||
this_window,
|
this_window,
|
||||||
@@ -138,54 +305,54 @@ impl Dnd {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn convert_selection(&self, window: xproto::Window, time: xproto::Timestamp) {
|
pub fn convert_selection(
|
||||||
|
&self,
|
||||||
|
window: xproto::Window,
|
||||||
|
time: xproto::Timestamp,
|
||||||
|
new_type: xproto::Atom,
|
||||||
|
) {
|
||||||
let atoms = self.xconn.atoms();
|
let atoms = self.xconn.atoms();
|
||||||
self.xconn
|
self.xconn
|
||||||
.xcb_connection()
|
.xcb_connection()
|
||||||
.convert_selection(
|
// TODO: We store the converted selection back to `XdndSelection`. We should store to
|
||||||
window,
|
// some new place so that `XdndSelection` remains untouched.
|
||||||
atoms[XdndSelection],
|
.convert_selection(window, atoms[XdndSelection], new_type, atoms[XdndSelection], time)
|
||||||
atoms[TextUriList],
|
|
||||||
atoms[XdndSelection],
|
|
||||||
time,
|
|
||||||
)
|
|
||||||
.expect_then_ignore_error("Failed to send XdndSelection event")
|
.expect_then_ignore_error("Failed to send XdndSelection event")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn read_data(
|
pub unsafe fn send_status(
|
||||||
|
&self,
|
||||||
|
this_window: xproto::Window,
|
||||||
|
target_window: xproto::Window,
|
||||||
|
status: DndState,
|
||||||
|
) -> Result<(), X11Error> {
|
||||||
|
let atoms = self.xconn.atoms();
|
||||||
|
let (accepted, action) = match status {
|
||||||
|
DndState::Accepted => (1, atoms[XdndActionCopy]),
|
||||||
|
DndState::Rejected => (0, atoms[DndNone]),
|
||||||
|
};
|
||||||
|
self.xconn
|
||||||
|
.send_client_msg(target_window, target_window, atoms[XdndStatus] as _, None, [
|
||||||
|
this_window,
|
||||||
|
accepted,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
action as _,
|
||||||
|
])?
|
||||||
|
.ignore_error();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_data(
|
||||||
&self,
|
&self,
|
||||||
window: xproto::Window,
|
window: xproto::Window,
|
||||||
) -> Result<Vec<c_uchar>, util::GetPropertyError> {
|
type_: SelectionType,
|
||||||
|
) -> Result<SelectionReader, util::GetPropertyError> {
|
||||||
let atoms = self.xconn.atoms();
|
let atoms = self.xconn.atoms();
|
||||||
self.xconn.get_property(window, atoms[XdndSelection], atoms[TextUriList])
|
let type_atom = type_.atom();
|
||||||
}
|
let bytes = self.xconn.get_property(window, atoms[XdndSelection], type_atom)?;
|
||||||
|
|
||||||
pub fn parse_data(&self, data: &mut [c_uchar]) -> Result<Vec<PathBuf>, DndDataParseError> {
|
Ok(SelectionReader { type_, data: bytes })
|
||||||
if !data.is_empty() {
|
|
||||||
let mut path_list = Vec::new();
|
|
||||||
let decoded = percent_decode(data).decode_utf8()?.into_owned();
|
|
||||||
for uri in decoded.split("\r\n").filter(|u| !u.is_empty()) {
|
|
||||||
// The format is specified as protocol://host/path
|
|
||||||
// However, it's typically simply protocol:///path
|
|
||||||
let path_str = if uri.starts_with("file://") {
|
|
||||||
let path_str = uri.replace("file://", "");
|
|
||||||
if !path_str.starts_with('/') {
|
|
||||||
// A hostname is specified
|
|
||||||
// Supporting this case is beyond the scope of my mental health
|
|
||||||
return Err(DndDataParseError::HostnameSpecified(path_str));
|
|
||||||
}
|
|
||||||
path_str
|
|
||||||
} else {
|
|
||||||
// Only the file protocol is supported
|
|
||||||
return Err(DndDataParseError::UnexpectedProtocol(uri.to_owned()));
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = Path::new(&path_str).canonicalize()?;
|
|
||||||
path_list.push(path);
|
|
||||||
}
|
|
||||||
Ok(path_list)
|
|
||||||
} else {
|
|
||||||
Err(DndDataParseError::EmptyData)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ use tracing::warn;
|
|||||||
use winit_common::xkb::Context;
|
use winit_common::xkb::Context;
|
||||||
use winit_core::application::ApplicationHandler;
|
use winit_core::application::ApplicationHandler;
|
||||||
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
|
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
|
||||||
use winit_core::error::{EventLoopError, RequestError};
|
use winit_core::data_transfer::{DataTransfer, DataTransferId, TransferType};
|
||||||
|
use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
|
||||||
use winit_core::event::{DeviceId, StartCause, WindowEvent};
|
use winit_core::event::{DeviceId, StartCause, WindowEvent};
|
||||||
use winit_core::event_loop::pump_events::PumpStatus;
|
use winit_core::event_loop::pump_events::PumpStatus;
|
||||||
use winit_core::event_loop::{
|
use winit_core::event_loop::{
|
||||||
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
|
ActiveEventLoop as RootActiveEventLoop, AsyncRequestSerial, ControlFlow, DeviceEvents,
|
||||||
EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider,
|
DndAction, EventLoopProxy as CoreEventLoopProxy, EventLoopProxyProvider,
|
||||||
OwnedDisplayHandle as CoreOwnedDisplayHandle,
|
OwnedDisplayHandle as CoreOwnedDisplayHandle,
|
||||||
};
|
};
|
||||||
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
|
use winit_core::monitor::MonitorHandle as CoreMonitorHandle;
|
||||||
@@ -36,14 +37,17 @@ use x11rb::protocol::{xkb, xproto};
|
|||||||
use x11rb::x11_utils::X11Error as LogicalError;
|
use x11rb::x11_utils::X11Error as LogicalError;
|
||||||
use x11rb::xcb_ffi::ReplyOrIdError;
|
use x11rb::xcb_ffi::ReplyOrIdError;
|
||||||
|
|
||||||
use crate::atoms::*;
|
use crate::atoms::{
|
||||||
|
_NET_WM_PING, _NET_WM_SYNC_REQUEST, ABS_PRESSURE, ABS_TILT_X, ABS_TILT_Y, ABS_X, ABS_Y, Atoms,
|
||||||
|
WM_DELETE_WINDOW,
|
||||||
|
};
|
||||||
use crate::dnd::Dnd;
|
use crate::dnd::Dnd;
|
||||||
use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN};
|
use crate::event_processor::{EventProcessor, MAX_MOD_REPLAY_LEN};
|
||||||
use crate::ime::{self, Ime, ImeCreationError, ImeSender};
|
use crate::ime::{self, Ime, ImeCreationError, ImeSender};
|
||||||
use crate::util::{self, CustomCursor};
|
use crate::util::{self, CustomCursor};
|
||||||
use crate::window::{UnownedWindow, Window};
|
use crate::window::{UnownedWindow, Window};
|
||||||
use crate::xdisplay::{XConnection, XError, XNotSupported};
|
use crate::xdisplay::{XConnection, XError, XNotSupported};
|
||||||
use crate::{XlibErrorHook, ffi, xsettings};
|
use crate::{Selection, SelectionType, XlibErrorHook, ffi, xsettings};
|
||||||
|
|
||||||
// Xinput constants not defined in x11rb
|
// Xinput constants not defined in x11rb
|
||||||
pub(crate) const ALL_DEVICES: u16 = 0;
|
pub(crate) const ALL_DEVICES: u16 = 0;
|
||||||
@@ -168,6 +172,7 @@ impl<T> PeekableReceiver<T> {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ActiveEventLoop {
|
pub struct ActiveEventLoop {
|
||||||
pub(crate) xconn: Arc<XConnection>,
|
pub(crate) xconn: Arc<XConnection>,
|
||||||
|
pub(crate) dnd: RefCell<Dnd>,
|
||||||
pub(crate) wm_delete_window: xproto::Atom,
|
pub(crate) wm_delete_window: xproto::Atom,
|
||||||
pub(crate) net_wm_ping: xproto::Atom,
|
pub(crate) net_wm_ping: xproto::Atom,
|
||||||
pub(crate) net_wm_sync_request: xproto::Atom,
|
pub(crate) net_wm_sync_request: xproto::Atom,
|
||||||
@@ -226,8 +231,7 @@ impl EventLoop {
|
|||||||
let net_wm_ping = atoms[_NET_WM_PING];
|
let net_wm_ping = atoms[_NET_WM_PING];
|
||||||
let net_wm_sync_request = atoms[_NET_WM_SYNC_REQUEST];
|
let net_wm_sync_request = atoms[_NET_WM_SYNC_REQUEST];
|
||||||
|
|
||||||
let dnd = Dnd::new(Arc::clone(&xconn))
|
let dnd = Dnd::new(Arc::clone(&xconn)).into();
|
||||||
.expect("Failed to call XInternAtoms when initializing drag and drop");
|
|
||||||
|
|
||||||
let (ime_sender, ime_receiver) = mpsc::channel();
|
let (ime_sender, ime_receiver) = mpsc::channel();
|
||||||
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
|
let (ime_event_sender, ime_event_receiver) = mpsc::channel();
|
||||||
@@ -342,6 +346,7 @@ impl EventLoop {
|
|||||||
|
|
||||||
let window_target = ActiveEventLoop {
|
let window_target = ActiveEventLoop {
|
||||||
ime,
|
ime,
|
||||||
|
dnd,
|
||||||
root,
|
root,
|
||||||
control_flow: Cell::new(ControlFlow::default()),
|
control_flow: Cell::new(ControlFlow::default()),
|
||||||
exit: Cell::new(None),
|
exit: Cell::new(None),
|
||||||
@@ -368,7 +373,6 @@ impl EventLoop {
|
|||||||
|
|
||||||
let event_processor = EventProcessor {
|
let event_processor = EventProcessor {
|
||||||
target: window_target,
|
target: window_target,
|
||||||
dnd,
|
|
||||||
devices: Default::default(),
|
devices: Default::default(),
|
||||||
randr_event_offset,
|
randr_event_offset,
|
||||||
ime_receiver,
|
ime_receiver,
|
||||||
@@ -621,9 +625,8 @@ impl EventLoop {
|
|||||||
fn drain_events<A: ApplicationHandler>(&mut self, app: &mut A) {
|
fn drain_events<A: ApplicationHandler>(&mut self, app: &mut A) {
|
||||||
let mut xev = MaybeUninit::uninit();
|
let mut xev = MaybeUninit::uninit();
|
||||||
|
|
||||||
while unsafe { self.event_processor.poll_one_event(xev.as_mut_ptr()) } {
|
while let Some(xev) = self.event_processor.poll_one_event(&mut xev) {
|
||||||
let mut xev = unsafe { xev.assume_init() };
|
self.event_processor.process_event(xev, app);
|
||||||
self.event_processor.process_event(&mut xev, app);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,6 +762,90 @@ impl RootActiveEventLoop for ActiveEventLoop {
|
|||||||
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
|
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn data_transfer(&self, id: DataTransferId) -> Result<Box<dyn DataTransfer>, RequestError> {
|
||||||
|
let dnd = self.dnd.borrow();
|
||||||
|
|
||||||
|
if dnd.state().is_none_or(|state| state.transfer_id != id) {
|
||||||
|
return Err(RequestError::Ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(state) = dnd.state() else {
|
||||||
|
return Err(RequestError::Ignored);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Box::new(Selection::new(state.types.clone())))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_data_transfer(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
type_: &dyn TransferType,
|
||||||
|
) -> Result<AsyncRequestSerial, RequestError> {
|
||||||
|
let mut dnd = self.dnd.borrow_mut();
|
||||||
|
|
||||||
|
let serial = AsyncRequestSerial::get();
|
||||||
|
|
||||||
|
let type_ = type_
|
||||||
|
.cast_ref::<SelectionType>()
|
||||||
|
.or_else(|| dnd.find_type_by_hint(type_.hint()?))
|
||||||
|
.cloned()
|
||||||
|
.ok_or(RequestError::NotSupported(NotSupportedError::new("Unknown type hint")))?;
|
||||||
|
|
||||||
|
let new_convert_selection = {
|
||||||
|
let Some(state) = dnd.state_mut() else {
|
||||||
|
return Err(RequestError::Ignored);
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.transfer_id != id {
|
||||||
|
return Err(RequestError::NotSupported(NotSupportedError::new(
|
||||||
|
"Unknown data transfer",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's non-empty, assume that we're still waiting on some other fetch operation.
|
||||||
|
// The `SelectionNotify` handler will send a new `convert_selection` event if any
|
||||||
|
// more are on the stack.
|
||||||
|
let should_emit_convert_selection = state.pending_fetch_types.is_empty();
|
||||||
|
|
||||||
|
let atom = type_.atom();
|
||||||
|
|
||||||
|
state.pending_fetch_types.push_back((serial, type_));
|
||||||
|
|
||||||
|
should_emit_convert_selection.then_some((
|
||||||
|
state.target_window,
|
||||||
|
self.xconn.timestamp(),
|
||||||
|
atom,
|
||||||
|
))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((window, time, new_type)) = new_convert_selection {
|
||||||
|
// This results in the `SelectionNotify` event
|
||||||
|
dnd.convert_selection(window, time, new_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_valid_dnd_actions(
|
||||||
|
&self,
|
||||||
|
id: DataTransferId,
|
||||||
|
actions: &[DndAction],
|
||||||
|
) -> Result<(), RequestError> {
|
||||||
|
let mut dnd = self.dnd.borrow_mut();
|
||||||
|
|
||||||
|
let Some(state) = dnd.state_mut() else {
|
||||||
|
return Err(os_error!(UnknownDataTransfer(id)).into());
|
||||||
|
};
|
||||||
|
|
||||||
|
if state.transfer_id != id {
|
||||||
|
return Err(os_error!(UnknownDataTransfer(id)).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
state.accepted = !actions.is_empty();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
|
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
|
||||||
@@ -767,6 +854,19 @@ impl rwh_06::HasDisplayHandle for ActiveEventLoop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An operation was attempted on a data transfer ID, but that ID was invalid.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub struct UnknownDataTransfer(pub DataTransferId);
|
||||||
|
|
||||||
|
impl fmt::Display for UnknownDataTransfer {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let id = self.0.into_raw();
|
||||||
|
write!(f, "Unknown data transfer with ID {id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for UnknownDataTransfer {}
|
||||||
|
|
||||||
pub(crate) struct DeviceInfo<'a> {
|
pub(crate) struct DeviceInfo<'a> {
|
||||||
xconn: &'a XConnection,
|
xconn: &'a XConnection,
|
||||||
info: *const ffi::XIDeviceInfo,
|
info: *const ffi::XIDeviceInfo,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::mem::MaybeUninit;
|
||||||
use std::os::raw::{c_char, c_int, c_long, c_ulong};
|
use std::os::raw::{c_char, c_int, c_long, c_ulong};
|
||||||
use std::slice;
|
use std::slice;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use dpi::{PhysicalPosition, PhysicalSize};
|
use dpi::{PhysicalPosition, PhysicalSize};
|
||||||
|
use tracing::warn;
|
||||||
use winit_common::xkb::{self, Context, XkbState};
|
use winit_common::xkb::{self, Context, XkbState};
|
||||||
use winit_core::application::ApplicationHandler;
|
use winit_core::application::ApplicationHandler;
|
||||||
use winit_core::event::{
|
use winit_core::event::{
|
||||||
@@ -12,6 +14,7 @@ use winit_core::event::{
|
|||||||
MouseScrollDelta, PointerKind, PointerSource, RawKeyEvent, SurfaceSizeWriter, TouchPhase,
|
MouseScrollDelta, PointerKind, PointerSource, RawKeyEvent, SurfaceSizeWriter, TouchPhase,
|
||||||
WindowEvent,
|
WindowEvent,
|
||||||
};
|
};
|
||||||
|
use winit_core::event_loop::DndAction;
|
||||||
use winit_core::keyboard::ModifiersState;
|
use winit_core::keyboard::ModifiersState;
|
||||||
use winit_core::window::WindowId;
|
use winit_core::window::WindowId;
|
||||||
use x11_dl::xinput2::{
|
use x11_dl::xinput2::{
|
||||||
@@ -31,7 +34,7 @@ use x11rb::x11_utils::{ExtensionInformation, Serialize};
|
|||||||
use xkbcommon_dl::xkb_mod_mask_t;
|
use xkbcommon_dl::xkb_mod_mask_t;
|
||||||
|
|
||||||
use crate::atoms::*;
|
use crate::atoms::*;
|
||||||
use crate::dnd::{Dnd, DndState};
|
use crate::dnd::{DndState, SelectionType};
|
||||||
use crate::event_loop::{
|
use crate::event_loop::{
|
||||||
ALL_DEVICES, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType,
|
ALL_DEVICES, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType,
|
||||||
ScrollOrientation, mkdid, mkwid,
|
ScrollOrientation, mkdid, mkwid,
|
||||||
@@ -49,7 +52,6 @@ const KEYCODE_OFFSET: u8 = 8;
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EventProcessor {
|
pub struct EventProcessor {
|
||||||
pub dnd: Dnd,
|
|
||||||
pub ime_receiver: ImeReceiver,
|
pub ime_receiver: ImeReceiver,
|
||||||
pub ime_event_receiver: ImeEventReceiver,
|
pub ime_event_receiver: ImeEventReceiver,
|
||||||
pub randr_event_offset: u8,
|
pub randr_event_offset: u8,
|
||||||
@@ -294,7 +296,10 @@ impl EventProcessor {
|
|||||||
unsafe { (self.target.xconn.xlib.XPending)(self.target.xconn.display) != 0 }
|
unsafe { (self.target.xconn.xlib.XPending)(self.target.xconn.display) != 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn poll_one_event(&mut self, event_ptr: *mut XEvent) -> bool {
|
pub fn poll_one_event<'a>(
|
||||||
|
&mut self,
|
||||||
|
event_ptr: &'a mut MaybeUninit<XEvent>,
|
||||||
|
) -> Option<&'a mut XEvent> {
|
||||||
// This function is used to poll and remove a single event
|
// This function is used to poll and remove a single event
|
||||||
// from the Xlib event queue in a non-blocking, atomic way.
|
// from the Xlib event queue in a non-blocking, atomic way.
|
||||||
// XCheckIfEvent is non-blocking and removes events from queue.
|
// XCheckIfEvent is non-blocking and removes events from queue.
|
||||||
@@ -304,20 +309,21 @@ impl EventProcessor {
|
|||||||
unsafe extern "C" fn predicate(
|
unsafe extern "C" fn predicate(
|
||||||
_display: *mut XDisplay,
|
_display: *mut XDisplay,
|
||||||
_event: *mut XEvent,
|
_event: *mut XEvent,
|
||||||
_arg: *mut c_char,
|
_filter: *mut c_char,
|
||||||
) -> c_int {
|
) -> c_int {
|
||||||
// This predicate always returns "true" (1) to accept all events
|
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe {
|
let event_initialized = unsafe {
|
||||||
(self.target.xconn.xlib.XCheckIfEvent)(
|
(self.target.xconn.xlib.XCheckIfEvent)(
|
||||||
self.target.xconn.display,
|
self.target.xconn.display,
|
||||||
event_ptr,
|
event_ptr.as_mut_ptr(),
|
||||||
Some(predicate),
|
Some(predicate),
|
||||||
std::ptr::null_mut(),
|
std::ptr::null_mut(),
|
||||||
) != 0
|
) != 0
|
||||||
}
|
};
|
||||||
|
|
||||||
|
event_initialized.then(|| unsafe { event_ptr.assume_init_mut() })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_device(&self, device: xinput::DeviceId) {
|
pub fn init_device(&self, device: xinput::DeviceId) {
|
||||||
@@ -423,21 +429,41 @@ impl EventProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if xev.message_type == atoms[XdndEnter] as c_ulong {
|
if xev.message_type == atoms[XdndEnter] as c_ulong {
|
||||||
|
// Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event`
|
||||||
|
// never contending the lock.
|
||||||
|
let transfer_id = {
|
||||||
|
let mut dnd = self.target.dnd.borrow_mut();
|
||||||
let source_window = xev.data.get_long(0) as xproto::Window;
|
let source_window = xev.data.get_long(0) as xproto::Window;
|
||||||
let flags = xev.data.get_long(1);
|
let flags = xev.data.get_long(1);
|
||||||
|
|
||||||
let version = flags >> 24;
|
let version = flags >> 24;
|
||||||
self.dnd.version = Some(version);
|
|
||||||
let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1;
|
let has_more_types = flags - (flags & (c_long::MAX - 1)) == 1;
|
||||||
if !has_more_types {
|
let types: Vec<_> = if !has_more_types {
|
||||||
let type_list = vec![
|
[
|
||||||
xev.data.get_long(2) as xproto::Atom,
|
xev.data.get_long(2) as xproto::Atom,
|
||||||
xev.data.get_long(3) as xproto::Atom,
|
xev.data.get_long(3) as xproto::Atom,
|
||||||
xev.data.get_long(4) as xproto::Atom,
|
xev.data.get_long(4) as xproto::Atom,
|
||||||
];
|
]
|
||||||
self.dnd.type_list = Some(type_list);
|
.map(|ty_atom| SelectionType::new(atoms, ty_atom))
|
||||||
} else if let Ok(more_types) = unsafe { self.dnd.get_type_list(source_window) } {
|
.into_iter()
|
||||||
self.dnd.type_list = Some(more_types);
|
.collect()
|
||||||
}
|
} else if let Ok(more_types) = unsafe { dnd.get_type_list(source_window) } {
|
||||||
|
more_types
|
||||||
|
.into_iter()
|
||||||
|
.map(|ty_atom| SelectionType::new(atoms, ty_atom))
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
dnd.init_state(version, source_window, window, types.into()).transfer_id
|
||||||
|
};
|
||||||
|
|
||||||
|
app.window_event(&self.target, window_id, WindowEvent::DragEntered {
|
||||||
|
id: transfer_id,
|
||||||
|
position: None,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,31 +490,17 @@ impl EventProcessor {
|
|||||||
.xconn
|
.xconn
|
||||||
.translate_coords(self.target.root, window, x, y)
|
.translate_coords(self.target.root, window, x, y)
|
||||||
.expect("Failed to translate window coordinates");
|
.expect("Failed to translate window coordinates");
|
||||||
self.dnd.position = PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64);
|
|
||||||
|
|
||||||
// By our own state flow, `version` should never be `None` at this point.
|
// Cautiously limit the scope of the `dnd` lock so we don't rely on `app.window_event`
|
||||||
let version = self.dnd.version.unwrap_or(5);
|
// never contending the lock.
|
||||||
|
let transfer_id = {
|
||||||
// Action is specified in versions 2 and up, though we don't need it anyway.
|
let dnd = self.target.dnd.borrow();
|
||||||
// let action = xev.data.get_long(4);
|
let Some(state) = dnd.state() else {
|
||||||
|
|
||||||
let accepted = if let Some(ref type_list) = self.dnd.type_list {
|
|
||||||
type_list.contains(&atoms[TextUriList])
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
if !accepted {
|
|
||||||
unsafe {
|
|
||||||
self.dnd
|
|
||||||
.send_status(window, source_window, DndState::Rejected)
|
|
||||||
.expect("Failed to send `XdndStatus` message.");
|
|
||||||
}
|
|
||||||
self.dnd.reset();
|
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
// By our own state flow, `state` should never be `None` at this point.
|
||||||
|
let version = state.version;
|
||||||
|
|
||||||
self.dnd.source_window = Some(source_window);
|
|
||||||
let time = if version == 0 {
|
let time = if version == 0 {
|
||||||
// In version 0, time isn't specified
|
// In version 0, time isn't specified
|
||||||
x11rb::CURRENT_TIME
|
x11rb::CURRENT_TIME
|
||||||
@@ -499,86 +511,183 @@ impl EventProcessor {
|
|||||||
// Log this timestamp.
|
// Log this timestamp.
|
||||||
self.target.xconn.set_timestamp(time);
|
self.target.xconn.set_timestamp(time);
|
||||||
|
|
||||||
// This results in the `SelectionNotify` event below
|
|
||||||
unsafe {
|
unsafe {
|
||||||
self.dnd.convert_selection(window, time);
|
dnd.send_status(
|
||||||
}
|
window,
|
||||||
|
source_window,
|
||||||
unsafe {
|
if state.accepted { DndState::Accepted } else { DndState::Rejected },
|
||||||
self.dnd
|
)
|
||||||
.send_status(window, source_window, DndState::Accepted)
|
|
||||||
.expect("Failed to send `XdndStatus` message.");
|
.expect("Failed to send `XdndStatus` message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.transfer_id
|
||||||
|
};
|
||||||
|
|
||||||
|
app.window_event(&self.target, window_id, WindowEvent::DragPosition {
|
||||||
|
id: transfer_id,
|
||||||
|
position: PhysicalPosition::new(coords.dst_x as f64, coords.dst_y as f64),
|
||||||
|
// `Copy` is the default. Other actions are possible in X11, but the specification
|
||||||
|
// does not properly explain how to implement them (only giving a vague description
|
||||||
|
// of `XdndMove`). For simplicity's sake, we simply do not implement non-copy drag
|
||||||
|
// on X11.
|
||||||
|
// See https://www.freedesktop.org/wiki/Specifications/XDND/
|
||||||
|
proposed_action: Some(DndAction::Copy),
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if xev.message_type == atoms[XdndDrop] as c_ulong {
|
if xev.message_type == atoms[XdndDrop] as c_ulong {
|
||||||
let (source_window, state) = if let Some(source_window) = self.dnd.source_window {
|
let (source_window, transfer_id) = {
|
||||||
if let Some(Ok(ref path_list)) = self.dnd.result {
|
let dnd = self.target.dnd.borrow();
|
||||||
let event = WindowEvent::DragDropped {
|
let Some(state) = dnd.state() else {
|
||||||
paths: path_list.iter().map(Into::into).collect(),
|
warn!("Received `XdndDrop` without `XdndEnter`");
|
||||||
position: self.dnd.position,
|
return;
|
||||||
};
|
};
|
||||||
app.window_event(&self.target, window_id, event);
|
let source_window = state.source_window;
|
||||||
}
|
|
||||||
(source_window, DndState::Accepted)
|
(source_window, state.transfer_id)
|
||||||
} else {
|
|
||||||
// `source_window` won't be part of our DND state if we already rejected the drop in
|
|
||||||
// our `XdndPosition` handler.
|
|
||||||
let source_window = xev.data.get_long(0) as xproto::Window;
|
|
||||||
(source_window, DndState::Rejected)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
app.window_event(
|
||||||
|
&self.target,
|
||||||
|
window_id,
|
||||||
|
// TODO
|
||||||
|
WindowEvent::DragDropped {
|
||||||
|
id: transfer_id,
|
||||||
|
// `Copy` is the default. Other actions are possible in X11, but the
|
||||||
|
// specification does not properly explain how to implement
|
||||||
|
// them (only giving a vague description of `XdndMove`). For
|
||||||
|
// simplicity's sake, we simply do not implement non-copy drag
|
||||||
|
// on X11.
|
||||||
|
// See https://www.freedesktop.org/wiki/Specifications/XDND/
|
||||||
|
proposed_action: Some(DndAction::Copy),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut dnd = self.target.dnd.borrow_mut();
|
||||||
|
|
||||||
|
if let Some(state) =
|
||||||
|
dnd.state_mut().filter(|state| !state.pending_fetch_types.is_empty())
|
||||||
|
{
|
||||||
|
state.finished = Some((window, source_window));
|
||||||
|
} else {
|
||||||
unsafe {
|
unsafe {
|
||||||
self.dnd
|
dnd.send_finished(window, source_window)
|
||||||
.send_finished(window, source_window, state)
|
|
||||||
.expect("Failed to send `XdndFinished` message.");
|
.expect("Failed to send `XdndFinished` message.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.dnd.reset();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if xev.message_type == atoms[XdndLeave] as c_ulong {
|
if xev.message_type == atoms[XdndLeave] as c_ulong {
|
||||||
if self.dnd.dragging {
|
let dnd = self.target.dnd.borrow();
|
||||||
let event = WindowEvent::DragLeft { position: Some(self.dnd.position) };
|
let Some(state) = dnd.state() else {
|
||||||
app.window_event(&self.target, window_id, event);
|
return;
|
||||||
}
|
};
|
||||||
self.dnd.reset();
|
app.window_event(&self.target, window_id, WindowEvent::DragLeft {
|
||||||
|
id: state.transfer_id,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn selection_notify(&mut self, xev: &XSelectionEvent, app: &mut dyn ApplicationHandler) {
|
fn selection_notify(&mut self, xev: &XSelectionEvent, app: &mut dyn ApplicationHandler) {
|
||||||
let atoms = self.target.xconn.atoms();
|
let atoms = self.target.xconn.atoms();
|
||||||
|
|
||||||
let window = xev.requestor as xproto::Window;
|
let xwindow = xev.requestor as xproto::Window;
|
||||||
let window_id = mkwid(window);
|
|
||||||
|
|
||||||
// Set the timestamp.
|
// Set the timestamp.
|
||||||
self.target.xconn.set_timestamp(xev.time as xproto::Timestamp);
|
self.target.xconn.set_timestamp(xev.time as xproto::Timestamp);
|
||||||
|
|
||||||
|
// For now, winit only supports selections for drag-and-drop. This should be changed
|
||||||
|
// when clipboard support is implemented.
|
||||||
if xev.property != atoms[XdndSelection] as c_ulong {
|
if xev.property != atoms[XdndSelection] as c_ulong {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is where we receive data from drag and drop
|
let (transfer_id, serial, type_) = {
|
||||||
self.dnd.result = None;
|
let Some(state) = self.target.dnd.get_mut().state_mut() else {
|
||||||
if let Ok(mut data) = unsafe { self.dnd.read_data(window) } {
|
return;
|
||||||
let parse_result = self.dnd.parse_data(&mut data);
|
|
||||||
|
|
||||||
if let Ok(ref path_list) = parse_result {
|
|
||||||
let event = if self.dnd.dragging {
|
|
||||||
WindowEvent::DragMoved { position: self.dnd.position }
|
|
||||||
} else {
|
|
||||||
let paths = path_list.iter().map(Into::into).collect();
|
|
||||||
self.dnd.dragging = true;
|
|
||||||
WindowEvent::DragEntered { paths, position: self.dnd.position }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
app.window_event(&self.target, window_id, event);
|
let Some((serial, type_)) = state.pending_fetch_types.pop_front() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Annoyingly, `xproto::Atom` and `x11_dl::Atom` are different on 64-bit
|
||||||
|
// but the same on 32-bit, so just casting to `u32` will cause "casting
|
||||||
|
// to same type" clippy warnings when compiled as 32-bit.
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
let target_type = xev.target;
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
let target_type = xev.target as u32;
|
||||||
|
|
||||||
|
if target_type != type_.atom() {
|
||||||
|
let get_name = |atom| {
|
||||||
|
self.target
|
||||||
|
.xconn
|
||||||
|
.xcb_connection()
|
||||||
|
.get_atom_name(atom)
|
||||||
|
.ok()
|
||||||
|
.and_then(|cookie| cookie.reply().ok())
|
||||||
|
.and_then(|reply| String::from_utf8(reply.name).ok())
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_type_name = get_name(type_.atom());
|
||||||
|
let found_type_name = get_name(xev.type_ as _);
|
||||||
|
|
||||||
|
let extra_context = match (expected_type_name, found_type_name) {
|
||||||
|
(Some(expected), Some(found)) => {
|
||||||
|
format!(" (expected {expected}, found {found})")
|
||||||
|
},
|
||||||
|
(Some(expected), None) => format!(" (expected {expected})"),
|
||||||
|
(None, Some(found)) => format!(" (found {found})"),
|
||||||
|
(None, None) => "".to_string(),
|
||||||
|
};
|
||||||
|
warn!(
|
||||||
|
"Received `SelectionNotify` with unexpected type{extra_context}. Continuing, \
|
||||||
|
but this may be a bug."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dnd.result = Some(parse_result);
|
(state.transfer_id, serial, type_)
|
||||||
|
};
|
||||||
|
|
||||||
|
let value = match self.target.dnd.borrow().read_data(xwindow, type_) {
|
||||||
|
Ok(value) => Arc::new(value),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to read selection: {err}");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_id = mkwid(xwindow);
|
||||||
|
|
||||||
|
app.window_event(&self.target, window_id, WindowEvent::DataTransferReceived {
|
||||||
|
id: transfer_id,
|
||||||
|
serial,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
let dnd = self.target.dnd.borrow();
|
||||||
|
|
||||||
|
// If we have another fetch pending, request it from the drag source window
|
||||||
|
if let Some((window, type_)) = dnd.state().and_then(|state| {
|
||||||
|
state
|
||||||
|
.pending_fetch_types
|
||||||
|
.front()
|
||||||
|
.cloned()
|
||||||
|
.map(|(_, type_)| (state.target_window, type_))
|
||||||
|
}) {
|
||||||
|
dnd.convert_selection(window, self.target.xconn.timestamp(), type_.atom());
|
||||||
|
} else if let Some((this_window, target_window)) =
|
||||||
|
dnd.state().and_then(|state| state.finished)
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
dnd.send_finished(this_window, target_window)
|
||||||
|
.expect("Failed to send `XdndFinished` message.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ mod window;
|
|||||||
mod xdisplay;
|
mod xdisplay;
|
||||||
mod xsettings;
|
mod xsettings;
|
||||||
|
|
||||||
|
pub use dnd::{Selection, SelectionReader, SelectionType, UriListParseError};
|
||||||
|
|
||||||
/// X window type. Maps directly to
|
/// X window type. Maps directly to
|
||||||
/// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html).
|
/// [`_NET_WM_WINDOW_TYPE`](https://specifications.freedesktop.org/wm-spec/wm-spec-1.5.html).
|
||||||
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub enum GetPropertyError {
|
|||||||
X11rbError(Arc<ReplyError>),
|
X11rbError(Arc<ReplyError>),
|
||||||
TypeMismatch(xproto::Atom),
|
TypeMismatch(xproto::Atom),
|
||||||
FormatMismatch(c_int),
|
FormatMismatch(c_int),
|
||||||
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetPropertyError {
|
impl GetPropertyError {
|
||||||
@@ -41,6 +42,7 @@ impl fmt::Display for GetPropertyError {
|
|||||||
GetPropertyError::X11rbError(err) => err.fmt(f),
|
GetPropertyError::X11rbError(err) => err.fmt(f),
|
||||||
GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"),
|
GetPropertyError::TypeMismatch(err) => write!(f, "type mismatch: {err}"),
|
||||||
GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"),
|
GetPropertyError::FormatMismatch(err) => write!(f, "format mismatch: {err}"),
|
||||||
|
GetPropertyError::Unknown => write!(f, "internal error"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,14 @@ use x11rb::protocol::sync::{ConnectionExt as _, Int64};
|
|||||||
use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle};
|
use x11rb::protocol::xproto::{self, ClipOrdering, ConnectionExt as _, Rectangle};
|
||||||
use x11rb::protocol::{randr, xinput};
|
use x11rb::protocol::{randr, xinput};
|
||||||
|
|
||||||
use crate::atoms::*;
|
use crate::atoms::{
|
||||||
|
_GTK_THEME_VARIANT, _NET_ACTIVE_WINDOW, _NET_WM_ICON, _NET_WM_MOVERESIZE, _NET_WM_NAME,
|
||||||
|
_NET_WM_PID, _NET_WM_PING, _NET_WM_STATE, _NET_WM_STATE_ABOVE, _NET_WM_STATE_BELOW,
|
||||||
|
_NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_HIDDEN, _NET_WM_STATE_MAXIMIZED_HORZ,
|
||||||
|
_NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_SYNC_REQUEST, _NET_WM_SYNC_REQUEST_COUNTER,
|
||||||
|
_NET_WM_WINDOW_TYPE, _XEMBED, AtomName, CARD32, UTF8_STRING, WM_CHANGE_STATE,
|
||||||
|
WM_CLIENT_MACHINE, WM_DELETE_WINDOW, WM_PROTOCOLS, WM_STATE, XdndAware,
|
||||||
|
};
|
||||||
use crate::event_loop::{
|
use crate::event_loop::{
|
||||||
ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie,
|
ALL_MASTER_DEVICES, ActivationItem, ActiveEventLoop, CookieResultExt, ICONIC_STATE, VoidCookie,
|
||||||
WakeSender, X11Error, xinput_fp1616_to_float,
|
WakeSender, X11Error, xinput_fp1616_to_float,
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
use cfg_aliases::cfg_aliases;
|
use cfg_aliases::cfg_aliases;
|
||||||
|
|
||||||
// Only relevant for examples and Winit, our usage of println! is fine here.
|
#[allow(
|
||||||
#[allow(clippy::disallowed_macros)]
|
clippy::disallowed_macros,
|
||||||
|
reason = "Only relevant for examples and Winit, our usage of println! is fine here."
|
||||||
|
)]
|
||||||
|
#[allow(
|
||||||
|
semicolon_in_expressions_from_macros,
|
||||||
|
reason = "This is a future incompatibility lint and we currently use cfg_aliases 0.2.1, which \
|
||||||
|
has not been updated to resolve the latest lints"
|
||||||
|
)]
|
||||||
fn main() {
|
fn main() {
|
||||||
// Dummy invocation to enable change-tracking in build scripts.
|
// Dummy invocation to enable change-tracking in build scripts.
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use image::imageops::FilterType;
|
||||||
|
use image::{DynamicImage, GenericImageView, RgbImage};
|
||||||
use softbuffer::{Context, Surface};
|
use softbuffer::{Context, Surface};
|
||||||
use tracing::info;
|
use tracing::{error, info, warn};
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::event::WindowEvent;
|
use winit::data_transfer::{DataTransferId, DataTransferSendBuilder, SendData, TypeHint};
|
||||||
use winit::event_loop::{ActiveEventLoop, EventLoop, OwnedDisplayHandle};
|
use winit::event::{ButtonSource, MouseButton, WindowEvent};
|
||||||
|
use winit::event_loop::{
|
||||||
|
ActiveEventLoop, AsyncRequestSerial, DndAction, DragIcon, EventLoop, OwnedDisplayHandle,
|
||||||
|
};
|
||||||
|
use winit::icon::{Icon, RgbaIcon};
|
||||||
use winit::window::{Window, WindowAttributes, WindowId};
|
use winit::window::{Window, WindowAttributes, WindowId};
|
||||||
|
|
||||||
#[path = "util/fill.rs"]
|
#[path = "util/fill.rs"]
|
||||||
@@ -17,20 +25,55 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
let event_loop = EventLoop::new()?;
|
let event_loop = EventLoop::new()?;
|
||||||
|
|
||||||
let app = Application::default();
|
let app = Application::new();
|
||||||
Ok(event_loop.run_app(app)?)
|
Ok(event_loop.run_app(app)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Application state and event handling.
|
/// Application state and event handling.
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug)]
|
||||||
struct Application {
|
struct Application {
|
||||||
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
|
surface: Option<Surface<OwnedDisplayHandle, Box<dyn Window>>>,
|
||||||
|
last_dnd_fetch: Option<AsyncRequestSerial>,
|
||||||
|
last_drag_start: Option<DataTransferId>,
|
||||||
|
drag_icon: (Icon, i32, i32),
|
||||||
|
drag_image_data: Arc<RgbImage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DRAG_IMAGE: &[u8] = include_bytes!("data/icon.png");
|
||||||
|
|
||||||
|
impl Application {
|
||||||
|
fn new() -> Self {
|
||||||
|
let drag_icon = load_icon(DRAG_IMAGE);
|
||||||
|
let drag_image_data = Arc::new(image::load_from_memory(DRAG_IMAGE).unwrap().into_rgb8());
|
||||||
|
Self {
|
||||||
|
surface: None,
|
||||||
|
last_dnd_fetch: None,
|
||||||
|
last_drag_start: None,
|
||||||
|
drag_icon,
|
||||||
|
drag_image_data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icon(bytes: &[u8]) -> (Icon, i32, i32) {
|
||||||
|
let (icon_rgba, icon_width, icon_height) = {
|
||||||
|
let image = image::load_from_memory(bytes).unwrap().into_rgba8();
|
||||||
|
let (width, height) = image.dimensions();
|
||||||
|
let rgba = image.into_raw();
|
||||||
|
(rgba, width, height)
|
||||||
|
};
|
||||||
|
(
|
||||||
|
RgbaIcon::new(icon_rgba, icon_width, icon_height).expect("Failed to open icon").into(),
|
||||||
|
-(icon_width as i32) / 2,
|
||||||
|
-(icon_height as i32) / 2,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationHandler for Application {
|
impl ApplicationHandler for Application {
|
||||||
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
fn can_create_surfaces(&mut self, event_loop: &dyn ActiveEventLoop) {
|
||||||
let window_attributes =
|
let window_attributes =
|
||||||
WindowAttributes::default().with_title("Drag and drop files on me!");
|
WindowAttributes::default().with_title("Drag and drop files, text or HTML onto me!");
|
||||||
|
|
||||||
let window = event_loop.create_window(window_attributes).unwrap();
|
let window = event_loop.create_window(window_attributes).unwrap();
|
||||||
let context = Context::new(event_loop.owned_display_handle()).unwrap();
|
let context = Context::new(event_loop.owned_display_handle()).unwrap();
|
||||||
let surface = Surface::new(&context, window).unwrap();
|
let surface = Surface::new(&context, window).unwrap();
|
||||||
@@ -40,14 +83,168 @@ impl ApplicationHandler for Application {
|
|||||||
fn window_event(
|
fn window_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &dyn ActiveEventLoop,
|
event_loop: &dyn ActiveEventLoop,
|
||||||
_window_id: WindowId,
|
window_id: WindowId,
|
||||||
event: WindowEvent,
|
event: WindowEvent,
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
WindowEvent::DragLeft { .. }
|
WindowEvent::PointerButton { button: ButtonSource::Mouse(button), state, .. }
|
||||||
| WindowEvent::DragEntered { .. }
|
if button == MouseButton::Left && state.is_pressed() =>
|
||||||
| WindowEvent::DragMoved { .. }
|
{
|
||||||
| WindowEvent::DragDropped { .. } => {
|
let (icon, offset_x, offset_y) = self.drag_icon.clone();
|
||||||
|
|
||||||
|
// In a real application, you probably wouldn't advertise so many types.
|
||||||
|
// Depending on platform and destination application, different options may be
|
||||||
|
// chosen.
|
||||||
|
let result = event_loop.start_drag(
|
||||||
|
window_id,
|
||||||
|
DataTransferSendBuilder::new(self.drag_image_data.clone())
|
||||||
|
.with_type(TypeHint::UriList, |_, _| {
|
||||||
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let root = manifest_dir.parent().unwrap();
|
||||||
|
let this_file = root.join(file!());
|
||||||
|
let icon_file = this_file.parent().unwrap().join("data/icon.png");
|
||||||
|
|
||||||
|
SendData::from_file_paths([icon_file])
|
||||||
|
})
|
||||||
|
.with_type(TypeHint::Plaintext, |_, _| Some("Winit example".to_string()))
|
||||||
|
.with_type(TypeHint::Html, |_, _| {
|
||||||
|
Some("<span><strong>Winit</strong> example</span>".to_string())
|
||||||
|
})
|
||||||
|
// You can advertise a `TypeHint` that can match many types, and switch
|
||||||
|
// inside the callback. For example, this will match any image type.
|
||||||
|
// This may be desirable on some platforms which restrict the set of
|
||||||
|
// image types that can be sent.
|
||||||
|
.with_type(TypeHint::Image { extension_hint: None }, |image, ty| {
|
||||||
|
let hint = ty.hint()?;
|
||||||
|
match hint {
|
||||||
|
TypeHint::Image { extension_hint } => {
|
||||||
|
let image = DynamicImage::from((**image).clone());
|
||||||
|
let (w, h) = image.dimensions();
|
||||||
|
let image = image
|
||||||
|
.resize(w * 8, h * 8, FilterType::Gaussian)
|
||||||
|
.into_rgb8();
|
||||||
|
let ext = extension_hint.unwrap_or("png");
|
||||||
|
info!("Destination requested image as {ext}, converting...");
|
||||||
|
let format = image::ImageFormat::from_extension(ext)?;
|
||||||
|
let mut out_buf = Vec::new();
|
||||||
|
let mut out_writer = std::io::Cursor::new(&mut out_buf);
|
||||||
|
|
||||||
|
image.write_to(&mut out_writer, format).ok()?;
|
||||||
|
|
||||||
|
Some(out_buf)
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
&[DndAction::Move, DndAction::Copy],
|
||||||
|
Some(DragIcon { icon, offset_x, offset_y }),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.last_drag_start = result.ok();
|
||||||
|
},
|
||||||
|
WindowEvent::DragLeft { .. } => {
|
||||||
|
info!("{event:?}");
|
||||||
|
},
|
||||||
|
WindowEvent::DataTransferReceived { ref value, serial, .. } => {
|
||||||
|
assert_eq!(self.last_dnd_fetch, Some(serial));
|
||||||
|
|
||||||
|
match value.type_().hint() {
|
||||||
|
Some(TypeHint::Plaintext | TypeHint::Html) => {
|
||||||
|
let Ok(text) = value.try_as_string() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
info!("{text:?}");
|
||||||
|
},
|
||||||
|
Some(TypeHint::UriList) => {
|
||||||
|
let Ok(uris) = value.try_as_uris() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
info!("URIs: {uris:#?}");
|
||||||
|
|
||||||
|
// If you only want to support dropping files, rather than arbitrary URIs,
|
||||||
|
// you can use the `try_as_file_paths` helper method.
|
||||||
|
let Ok(uris_as_paths) = value.try_as_file_paths() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
info!("URIs as file paths: {uris_as_paths:#?}");
|
||||||
|
},
|
||||||
|
Some(TypeHint::Image { extension_hint: ext }) => {
|
||||||
|
let Ok(bytes) = value.try_as_bytes() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let format = ext.and_then(image::ImageFormat::from_extension);
|
||||||
|
|
||||||
|
let reader = std::io::Cursor::new(&bytes[..]);
|
||||||
|
let reader = match format {
|
||||||
|
Some(fmt) => image::ImageReader::with_format(reader, fmt),
|
||||||
|
None => image::ImageReader::new(reader),
|
||||||
|
};
|
||||||
|
|
||||||
|
match reader.decode() {
|
||||||
|
Ok(image) => {
|
||||||
|
let width = image.width();
|
||||||
|
let height = image.height();
|
||||||
|
info!("Received image ({width}x{height})");
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to decode image: {err}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
unreachable!("Received a type we didn't ask for!");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
WindowEvent::DragPosition { .. } => {
|
||||||
|
info!("{event:?}");
|
||||||
|
},
|
||||||
|
WindowEvent::DragDropped { .. } => {
|
||||||
|
info!("{event:?}");
|
||||||
|
},
|
||||||
|
WindowEvent::DragEntered { id, .. } => {
|
||||||
|
info!("{event:?}");
|
||||||
|
|
||||||
|
let data_transfer = match event_loop.data_transfer(id) {
|
||||||
|
Ok(dt) => dt,
|
||||||
|
Err(e) => {
|
||||||
|
error!("{e}");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Types: {:#?}", data_transfer.available_types());
|
||||||
|
|
||||||
|
let readable_image_types = image::ImageFormat::all()
|
||||||
|
.filter(|fmt| fmt.reading_enabled())
|
||||||
|
.filter_map(|fmt| {
|
||||||
|
let ext = fmt.extensions_str().first()?;
|
||||||
|
|
||||||
|
Some(TypeHint::Image { extension_hint: Some(ext) })
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut valid_types = readable_image_types.chain([
|
||||||
|
TypeHint::Html,
|
||||||
|
TypeHint::UriList,
|
||||||
|
TypeHint::Plaintext,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let valid_type = valid_types.find(|ty| data_transfer.has_type(ty));
|
||||||
|
|
||||||
|
let Some(type_) = valid_type else {
|
||||||
|
event_loop.set_valid_dnd_actions(id, &[]).unwrap();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
event_loop.set_valid_dnd_actions(id, &[DndAction::Move, DndAction::Copy]).unwrap();
|
||||||
|
|
||||||
|
self.last_dnd_fetch = event_loop.fetch_data_transfer(id, &type_).ok();
|
||||||
|
},
|
||||||
|
WindowEvent::OutgoingDragDropped { .. } => {
|
||||||
|
info!("{event:?}");
|
||||||
|
},
|
||||||
|
WindowEvent::OutgoingDragCanceled { .. } => {
|
||||||
info!("{event:?}");
|
info!("{event:?}");
|
||||||
},
|
},
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
|
|||||||
@@ -292,7 +292,9 @@ pub use rwh_06 as raw_window_handle;
|
|||||||
#[cfg(any(doc, doctest, test))]
|
#[cfg(any(doc, doctest, test))]
|
||||||
pub mod changelog;
|
pub mod changelog;
|
||||||
pub mod event_loop;
|
pub mod event_loop;
|
||||||
pub use winit_core::{application, cursor, error, event, icon, keyboard, monitor, window};
|
pub use winit_core::{
|
||||||
|
application, cursor, data_transfer, error, event, icon, keyboard, monitor, window,
|
||||||
|
};
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod os_error;
|
mod os_error;
|
||||||
mod platform_impl;
|
mod platform_impl;
|
||||||
|
|||||||
Reference in New Issue
Block a user