diff --git a/Cargo.toml b/Cargo.toml index eb23f4e..162fe60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ xcb-util = { version = "0.3", features = ["icccm"] } nix = "0.22.0" [target.'cfg(target_os="windows")'.dependencies] -winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi"] } +winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi", "ole2", "oleidl", "shellapi", "winerror"] } uuid = { version = "0.8", features = ["v4"], optional = true } [target.'cfg(target_os="macos")'.dependencies] diff --git a/src/event.rs b/src/event.rs index 3c9ba5b..04d4908 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use keyboard_types::{KeyboardEvent, Modifiers}; use crate::{Point, WindowInfo}; @@ -32,7 +34,7 @@ pub enum ScrollDelta { }, } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum MouseEvent { /// The mouse cursor was moved CursorMoved { @@ -75,6 +77,35 @@ pub enum MouseEvent { /// /// May not be available on all platforms. CursorLeft, + + DragEntered { + /// The logical coordinates of the mouse position + position: Point, + /// The modifiers that were held down just before the event. + modifiers: Modifiers, + /// Data being dragged + data: DropData, + }, + + DragMoved { + /// The logical coordinates of the mouse position + position: Point, + /// The modifiers that were held down just before the event. + modifiers: Modifiers, + /// Data being dragged + data: DropData, + }, + + DragLeft, + + DragDropped { + /// The logical coordinates of the mouse position + position: Point, + /// The modifiers that were held down just before the event. + modifiers: Modifiers, + /// Data being dragged + data: DropData, + }, } #[derive(Debug, Clone)] @@ -92,6 +123,20 @@ pub enum Event { Window(WindowEvent), } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DropEffect { + Copy, + Move, + Link, + Scroll, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum DropData { + None, + Files(Vec), +} + /// Return value for [WindowHandler::on_event](`crate::WindowHandler::on_event()`), /// indicating whether the event was handled by your window or should be passed /// back to the platform. @@ -111,4 +156,7 @@ pub enum EventStatus { /// DAW functionality for playing piano keys with the keyboard while a /// plugin window is in focus. Ignored, + /// We are prepared to handle the data in the drag and dropping will + /// result in [DropEffect] + AcceptDrop(DropEffect), } diff --git a/src/win/drop_target.rs b/src/win/drop_target.rs new file mode 100644 index 0000000..bcfd036 --- /dev/null +++ b/src/win/drop_target.rs @@ -0,0 +1,268 @@ +use std::ffi::OsString; +use std::mem::transmute; +use std::os::windows::prelude::OsStringExt; +use std::ptr::null_mut; +use std::rc::{Weak, Rc}; + +use winapi::Interface; +use winapi::shared::guiddef::{REFIID, IsEqualIID}; +use winapi::shared::minwindef::{DWORD, WPARAM}; +use winapi::shared::ntdef::{HRESULT, ULONG}; +use winapi::shared::windef::POINTL; +use winapi::shared::winerror::{S_OK, E_NOINTERFACE, E_UNEXPECTED}; +use winapi::shared::wtypes::DVASPECT_CONTENT; +use winapi::um::objidl::{IDataObject, FORMATETC, TYMED_HGLOBAL, STGMEDIUM}; +use winapi::um::oleidl::{IDropTarget, IDropTargetVtbl, DROPEFFECT_COPY, DROPEFFECT_MOVE, DROPEFFECT_LINK, DROPEFFECT_SCROLL, DROPEFFECT_NONE}; +use winapi::um::shellapi::DragQueryFileW; +use winapi::um::unknwnbase::{IUnknownVtbl, IUnknown}; +use winapi::um::winuser::CF_HDROP; + +use crate::{Point, DropData, MouseEvent, Event, EventStatus, DropEffect, PhyPoint}; + +use super::WindowState; + +// These function pointers have to be stored in a (const) variable before they can be transmuted +// Transmuting is needed because winapi has a bug where the pt parameter has an incorrect +// type `*const POINTL` +const DRAG_ENTER_PTR: unsafe extern "system" fn(this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL, pdwEffect: *mut DWORD) -> HRESULT = DropTarget::drag_enter; +const DRAG_OVER_PTR: unsafe extern "system" fn(this: *mut IDropTarget, grfKeyState: DWORD, pt: POINTL, pdwEffect: *mut DWORD) -> HRESULT = DropTarget::drag_over; +const DROP_PTR: unsafe extern "system" fn(this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL, pdwEffect: *mut DWORD) -> HRESULT = DropTarget::drop; +const DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl { + parent: IUnknownVtbl { + QueryInterface: DropTarget::query_interface, + AddRef: DropTarget::add_ref, + Release: DropTarget::release, + }, + DragEnter: unsafe { transmute(DRAG_ENTER_PTR) }, + DragOver: unsafe { transmute(DRAG_OVER_PTR) }, + DragLeave: DropTarget::drag_leave, + Drop: unsafe { transmute(DROP_PTR) }, +}; + +#[repr(C)] +pub(super) struct DropTarget { + base: IDropTarget, + + window_state: Weak, + + // These are cached since DragOver and DragLeave callbacks don't provide them, + // and handling drag move events gets awkward on the client end otherwise + drag_position: Point, + drop_data: DropData, +} + +impl DropTarget { + pub(super) fn new(window_state: Weak) -> Self { + Self { + base: IDropTarget { lpVtbl: &DROP_TARGET_VTBL }, + + window_state, + + drag_position: Point::new(0.0, 0.0), + drop_data: DropData::None, + } + } + + fn on_event(&self, pdwEffect: Option<*mut DWORD>, event: MouseEvent) { + let Some(window_state) = self.window_state.upgrade() else { + return; + }; + + unsafe { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + let event = Event::Mouse(event); + let event_status = window_state.handler_mut().as_mut().unwrap().on_event(&mut window, event); + + if let Some(pdwEffect) = pdwEffect { + match event_status { + EventStatus::AcceptDrop(DropEffect::Copy) => *pdwEffect = DROPEFFECT_COPY, + EventStatus::AcceptDrop(DropEffect::Move) => *pdwEffect = DROPEFFECT_MOVE, + EventStatus::AcceptDrop(DropEffect::Link) => *pdwEffect = DROPEFFECT_LINK, + EventStatus::AcceptDrop(DropEffect::Scroll) => *pdwEffect = DROPEFFECT_SCROLL, + _ => *pdwEffect = DROPEFFECT_NONE, + } + } + } + } + + fn parse_coordinates(&mut self, pt: POINTL) { + let Some(window_state) = self.window_state.upgrade() else { + return; + }; + + let phy_point = PhyPoint::new(pt.x, pt.y); + self.drag_position = phy_point.to_logical(&window_state.window_info()); + } + + fn parse_drop_data(&mut self, data_object: &IDataObject) { + let format = FORMATETC { + cfFormat: CF_HDROP as u16, + ptd: null_mut(), + dwAspect: DVASPECT_CONTENT, + lindex: -1, + tymed: TYMED_HGLOBAL, + }; + + let mut medium = STGMEDIUM { + tymed: 0, + u: null_mut(), + pUnkForRelease: null_mut(), + }; + + unsafe { + let hresult = data_object.GetData(&format, &mut medium); + if hresult != S_OK { + self.drop_data = DropData::None; + return; + } + + let hdrop = transmute((*medium.u).hGlobal()); + + let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, null_mut(), 0); + if item_count == 0 { + self.drop_data = DropData::None; + return; + } + + let mut paths = Vec::with_capacity(item_count as usize); + + for i in 0..item_count { + let characters = DragQueryFileW(hdrop, i, null_mut(), 0); + let buffer_size = characters as usize + 1; + let mut buffer = Vec::::with_capacity(buffer_size); + + DragQueryFileW(hdrop, i, transmute(buffer.spare_capacity_mut().as_mut_ptr()), buffer_size as u32); + buffer.set_len(buffer_size); + + paths.push(OsString::from_wide(&buffer[..characters as usize]).into()) + } + + self.drop_data = DropData::Files(paths); + } + } + + unsafe extern "system" fn query_interface( + this: *mut IUnknown, + riid: REFIID, + ppvObject: *mut *mut winapi::ctypes::c_void, + ) -> HRESULT + { + if IsEqualIID(&*riid, &IUnknown::uuidof()) || IsEqualIID(&*riid, &IDropTarget::uuidof()){ + Self::add_ref(this); + *ppvObject = this as *mut winapi::ctypes::c_void; + return S_OK; + } + + return E_NOINTERFACE; + } + + unsafe extern "system" fn add_ref(this: *mut IUnknown) -> ULONG { + let arc = Rc::from_raw(this); + let result = Rc::strong_count(&arc) + 1; + let _ = Rc::into_raw(arc); + + Rc::increment_strong_count(this); + + result as ULONG + } + + unsafe extern "system" fn release(this: *mut IUnknown) -> ULONG { + let arc = Rc::from_raw(this); + let result = Rc::strong_count(&arc) - 1; + let _ = Rc::into_raw(arc); + + Rc::decrement_strong_count(this); + + result as ULONG + } + + unsafe extern "system" fn drag_enter( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: DWORD, + pt: POINTL, + pdwEffect: *mut DWORD, + ) -> HRESULT + { + let drop_target = &mut *(this as *mut DropTarget); + let Some(window_state) = drop_target.window_state.upgrade() else { + return E_UNEXPECTED; + }; + + let modifiers = window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM); + + drop_target.parse_coordinates(pt); + drop_target.parse_drop_data(&*pDataObj); + + let event = MouseEvent::DragEntered { + position: drop_target.drag_position, + modifiers, + data: drop_target.drop_data.clone(), + }; + + drop_target.on_event(Some(pdwEffect), event); + S_OK + } + + unsafe extern "system" fn drag_over( + this: *mut IDropTarget, + grfKeyState: DWORD, + pt: POINTL, + pdwEffect: *mut DWORD, + ) -> HRESULT + { + let drop_target = &mut *(this as *mut DropTarget); + let Some(window_state) = drop_target.window_state.upgrade() else { + return E_UNEXPECTED; + }; + + let modifiers = window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM); + + drop_target.parse_coordinates(pt); + + let event = MouseEvent::DragMoved { + position: drop_target.drag_position, + modifiers, + data: drop_target.drop_data.clone(), + }; + + drop_target.on_event(Some(pdwEffect), event); + S_OK + } + + unsafe extern "system" fn drag_leave(this: *mut IDropTarget) -> HRESULT { + let drop_target = &mut *(this as *mut DropTarget); + drop_target.on_event(None, MouseEvent::DragLeft); + S_OK + } + + unsafe extern "system" fn drop( + this: *mut IDropTarget, + pDataObj: *const IDataObject, + grfKeyState: DWORD, + pt: POINTL, + pdwEffect: *mut DWORD, + ) -> HRESULT + { + let drop_target = &mut *(this as *mut DropTarget); + let Some(window_state) = drop_target.window_state.upgrade() else { + return E_UNEXPECTED; + }; + + let modifiers = window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM); + + drop_target.parse_coordinates(pt); + drop_target.parse_drop_data(&*pDataObj); + + let event = MouseEvent::DragDropped { + position: drop_target.drag_position, + modifiers, + data: drop_target.drop_data.clone(), + }; + + drop_target.on_event(Some(pdwEffect), event); + S_OK + } +} diff --git a/src/win/mod.rs b/src/win/mod.rs index 1d76d14..f66c2bd 100644 --- a/src/win/mod.rs +++ b/src/win/mod.rs @@ -1,3 +1,4 @@ +mod drop_target; mod keyboard; mod window; diff --git a/src/win/window.rs b/src/win/window.rs index 2af1abe..2c68b99 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -2,6 +2,8 @@ use winapi::shared::guiddef::GUID; use winapi::shared::minwindef::{ATOM, FALSE, LPARAM, LRESULT, UINT, WPARAM}; use winapi::shared::windef::{HWND, RECT}; use winapi::um::combaseapi::CoCreateGuid; +use winapi::um::ole2::{RegisterDragDrop, OleInitialize, RevokeDragDrop}; +use winapi::um::oleidl::LPDROPTARGET; use winapi::um::winuser::{ AdjustWindowRectEx, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GetDpiForWindow, GetMessageW, GetWindowLongPtrW, LoadCursorW, PostMessageW, RegisterClassW, @@ -16,7 +18,7 @@ use winapi::um::winuser::{ XBUTTON1, XBUTTON2, }; -use std::cell::{Cell, RefCell}; +use std::cell::{Cell, RefCell, Ref, RefMut}; use std::collections::VecDeque; use std::ffi::{c_void, OsStr}; use std::marker::PhantomData; @@ -33,6 +35,7 @@ use crate::{ WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, }; +use super::drop_target::DropTarget; use super::keyboard::KeyboardState; #[cfg(feature = "opengl")] @@ -147,9 +150,10 @@ unsafe extern "system" fn wnd_proc( // NOTE: This is not handled in `wnd_proc_inner` because of the deferred task loop above if msg == WM_NCDESTROY { + RevokeDragDrop(hwnd); unregister_wnd_class((*window_state_ptr).window_class); SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); - drop(Box::from_raw(window_state_ptr)); + drop(Rc::from_raw(window_state_ptr)); } // The actual custom window proc has been moved to another function so we can always handle @@ -446,7 +450,7 @@ unsafe fn unregister_wnd_class(wnd_class: ATOM) { /// because of the Windows message loops' reentrant nature. Care still needs to be taken to prevent /// `handler` from indirectly triggering other events that would also need to be handled using /// `handler`. -struct WindowState { +pub(super) struct WindowState { /// The HWND belonging to this window. The window's actual state is stored in the `WindowState` /// struct associated with this HWND through `unsafe { GetWindowLongPtrW(self.hwnd, /// GWLP_USERDATA) } as *const WindowState`. @@ -458,6 +462,7 @@ struct WindowState { mouse_button_counter: Cell, // Initialized late so the `Window` can hold a reference to this `WindowState` handler: RefCell>>, + _drop_target: RefCell>>, scale_policy: WindowScalePolicy, dw_style: u32, @@ -473,10 +478,22 @@ struct WindowState { } impl WindowState { - fn create_window(&self) -> Window { + pub(super) fn create_window(&self) -> Window { Window { state: self } } + pub(super) fn window_info(&self) -> Ref { + self.window_info.borrow() + } + + pub(super) fn keyboard_state(&self) -> Ref { + self.keyboard_state.borrow() + } + + pub(super) fn handler_mut(&self) -> RefMut>> { + self.handler.borrow_mut() + } + /// Handle a deferred task as described in [`Self::deferred_tasks pub(self) fn handle_deferred_task(&self, task: WindowTask) { match task { @@ -517,7 +534,7 @@ impl WindowState { /// Tasks that must be deferred until the end of [`wnd_proc()`] to avoid reentrant `WindowState` /// borrows. See the docstring on [`WindowState::deferred_tasks`] for more information. #[derive(Debug, Clone)] -enum WindowTask { +pub(super) enum WindowTask { /// Resize the window to the given size. The size is in logical pixels. DPI scaling is applied /// automatically. Resize(Size), @@ -654,7 +671,7 @@ impl Window<'_> { let (parent_handle, window_handle) = ParentHandle::new(hwnd); let parent_handle = if parented { Some(parent_handle) } else { None }; - let window_state = Box::new(WindowState { + let window_state = Rc::new(WindowState { hwnd, window_class, window_info: RefCell::new(window_info), @@ -664,6 +681,7 @@ impl Window<'_> { // The Window refers to this `WindowState`, so this `handler` needs to be // initialized later handler: RefCell::new(None), + _drop_target: RefCell::new(None), scale_policy: options.scale, dw_style: flags, @@ -711,7 +729,13 @@ impl Window<'_> { None }; - SetWindowLongPtrW(hwnd, GWLP_USERDATA, Box::into_raw(window_state) as *const _ as _); + let drop_target = Rc::new(DropTarget::new(Rc::downgrade(&window_state))); + *window_state._drop_target.borrow_mut() = Some(drop_target.clone()); + + OleInitialize(null_mut()); + RegisterDragDrop(hwnd, Rc::as_ptr(&drop_target) as LPDROPTARGET); + + SetWindowLongPtrW(hwnd, GWLP_USERDATA, Rc::into_raw(window_state) as *const _ as _); SetTimer(hwnd, WIN_FRAME_TIMER, 15, None); if let Some(mut new_rect) = new_rect {