11//! Native drag interception for macOS via method swizzling on WryWebView.
22//!
3- //! Swizzles `draggingEntered:` and `draggingUpdated :` to:
3+ //! Swizzles `draggingEntered:`, `draggingUpdated:`, and `draggingExited :` to:
44//! 1. Read drag image dimensions via `enumerateDraggingItems` (for overlay suppression)
55//! 2. Read modifier key state via `[NSEvent modifierFlags]` (for copy/move detection)
6+ //! 3. Swap the OS drag image for self-drags (delegated to `drag_image_swap`)
67//!
78//! Events emitted:
89//! - `drag-image-size` `{ width, height }` — on drag enter
1415//! internal webview class or macOS deprecates APIs we rely on, the swizzle degrades gracefully:
1516//! - Drag image detection disabled → the DOM overlay is always shown (redundant but functional)
1617//! - Modifier key detection disabled → falls back to JS keydown/keyup (works when webview has focus)
18+ //! - Image swapping disabled → self-drags show the OS drag image over the window (functional)
1719//!
1820//! Rust panics inside swizzled functions are caught via `catch_unwind` to prevent crashes
1921//! across the FFI boundary. Warning messages include actionable guidance for future maintainers.
@@ -26,10 +28,12 @@ use std::sync::atomic::{AtomicBool, Ordering};
2628use objc2:: runtime:: { AnyClass , AnyObject , Bool , Imp , Sel } ;
2729use objc2:: { msg_send, sel} ;
2830use objc2_app_kit:: { NSDragOperation , NSDraggingItem , NSDraggingItemEnumerationOptions } ;
29- use objc2_foundation:: { NSDictionary , NSInteger , NSRect } ;
31+ use objc2_foundation:: { NSDictionary , NSInteger , NSRect , NSSize } ;
3032use serde:: Serialize ;
3133use tauri:: { AppHandle , Emitter } ;
3234
35+ use crate :: drag_image_swap;
36+
3337/// NSEventModifierFlagOption = 1 << 19 (Option/Alt key)
3438const NS_EVENT_MODIFIER_FLAG_OPTION : usize = 1 << 19 ;
3539
@@ -47,6 +51,7 @@ struct DragModifiers {
4751
4852static ORIGINAL_ENTERED_IMP : OnceLock < Imp > = OnceLock :: new ( ) ;
4953static ORIGINAL_UPDATED_IMP : OnceLock < Imp > = OnceLock :: new ( ) ;
54+ static ORIGINAL_EXITED_IMP : OnceLock < Imp > = OnceLock :: new ( ) ;
5055static APP_HANDLE : OnceLock < AppHandle > = OnceLock :: new ( ) ;
5156
5257/// Tracks previous alt state so we only emit `drag-modifiers` when it changes.
@@ -56,11 +61,12 @@ static LAST_ALT_HELD: AtomicBool = AtomicBool::new(false);
5661static WARNED_NSEVENT_MISSING : AtomicBool = AtomicBool :: new ( false ) ;
5762static WARNED_ENTERED_PANIC : AtomicBool = AtomicBool :: new ( false ) ;
5863static WARNED_UPDATED_PANIC : AtomicBool = AtomicBool :: new ( false ) ;
64+ static WARNED_EXITED_PANIC : AtomicBool = AtomicBool :: new ( false ) ;
5965static WARNED_NSARRAY_MISSING : AtomicBool = AtomicBool :: new ( false ) ;
6066static WARNED_DRAGGED_IMAGE_REMOVED : AtomicBool = AtomicBool :: new ( false ) ;
6167
6268/// Logs a warning message at most once per app session.
63- fn warn_once ( flag : & AtomicBool , msg : & str ) {
69+ pub ( crate ) fn warn_once ( flag : & AtomicBool , msg : & str ) {
6470 if !flag. swap ( true , Ordering :: Relaxed ) {
6571 log:: warn!( "{msg}" ) ;
6672 }
@@ -112,10 +118,27 @@ pub fn install(app_handle: AppHandle) {
112118 ) ;
113119 }
114120
121+ // Swizzle draggingExited: for self-drag image swapping (transparent → rich on window exit)
122+ if let Some ( method) = cls. instance_method ( sel ! ( draggingExited: ) ) {
123+ ORIGINAL_EXITED_IMP . set ( method. implementation ( ) ) . ok ( ) ;
124+ method. set_implementation ( std:: mem:: transmute :: < * const ( ) , Imp > (
125+ swizzled_dragging_exited as * const ( ) ,
126+ ) ) ;
127+ } else {
128+ log:: warn!(
129+ "drag_image_detection: draggingExited: not found on WryWebView — \
130+ drag image swapping on window exit is disabled. \
131+ Wry may have changed how it implements NSDraggingDestination; \
132+ check wry's drag-and-drop event handling in its ObjC layer."
133+ ) ;
134+ }
135+
115136 log:: info!( "drag_image_detection: swizzles installed on WryWebView" ) ;
116137 }
117138}
118139
140+ // --- Modifier key detection ---
141+
119142/// Reads the current Option/Alt key state from `[NSEvent modifierFlags]`.
120143/// This is a class method that reads hardware state — works even when the webview isn't focused.
121144/// Returns `false` if NSEvent can't be found (graceful degradation).
@@ -158,29 +181,46 @@ fn emit_modifiers_forced() {
158181
159182/// Forwards to wry's original `draggingEntered:`. Always safe to call — returns
160183/// `NSDragOperation::Copy` if the original wasn't saved (shouldn't happen in practice).
161- unsafe fn call_original_entered ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) -> usize { unsafe {
162- if let Some ( & original) = ORIGINAL_ENTERED_IMP . get ( ) {
163- let f = std:: mem:: transmute :: < Imp , unsafe extern "C-unwind" fn ( & AnyObject , Sel , & AnyObject ) -> usize > ( original) ;
164- f ( this, cmd, drag_info)
165- } else {
166- NSDragOperation :: Copy . 0
184+ unsafe fn call_original_entered ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) -> usize {
185+ unsafe {
186+ if let Some ( & original) = ORIGINAL_ENTERED_IMP . get ( ) {
187+ let f =
188+ std:: mem:: transmute :: < Imp , unsafe extern "C-unwind" fn ( & AnyObject , Sel , & AnyObject ) -> usize > ( original) ;
189+ f ( this, cmd, drag_info)
190+ } else {
191+ NSDragOperation :: Copy . 0
192+ }
167193 }
168- } }
194+ }
169195
170196/// Forwards to wry's original `draggingUpdated:`. Same fallback as above.
171- unsafe fn call_original_updated ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) -> usize { unsafe {
172- if let Some ( & original) = ORIGINAL_UPDATED_IMP . get ( ) {
173- let f = std:: mem:: transmute :: < Imp , unsafe extern "C-unwind" fn ( & AnyObject , Sel , & AnyObject ) -> usize > ( original) ;
174- f ( this, cmd, drag_info)
175- } else {
176- NSDragOperation :: Copy . 0
197+ unsafe fn call_original_updated ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) -> usize {
198+ unsafe {
199+ if let Some ( & original) = ORIGINAL_UPDATED_IMP . get ( ) {
200+ let f =
201+ std:: mem:: transmute :: < Imp , unsafe extern "C-unwind" fn ( & AnyObject , Sel , & AnyObject ) -> usize > ( original) ;
202+ f ( this, cmd, drag_info)
203+ } else {
204+ NSDragOperation :: Copy . 0
205+ }
177206 }
178- } }
207+ }
208+
209+ /// Forwards to wry's original `draggingExited:`. Returns void.
210+ /// If the original wasn't saved, this is a no-op (drag exit still works, wry just won't fire its handler).
211+ unsafe fn call_original_exited ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) {
212+ unsafe {
213+ if let Some ( & original) = ORIGINAL_EXITED_IMP . get ( ) {
214+ let f = std:: mem:: transmute :: < Imp , unsafe extern "C-unwind" fn ( & AnyObject , Sel , & AnyObject ) > ( original) ;
215+ f ( this, cmd, drag_info)
216+ }
217+ }
218+ }
179219
180220// --- draggingEntered: swizzle ---
181221
182222unsafe extern "C-unwind" fn swizzled_dragging_entered ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) -> usize {
183- // Our custom logic: read drag image size and emit modifier events.
223+ // Our custom logic: read drag image size, emit modifier events, and swap image for self-drags .
184224 // Wrapped in catch_unwind to prevent any unexpected Rust panic from crossing the FFI boundary
185225 // and crashing the app mid-drag.
186226 let result = catch_unwind ( AssertUnwindSafe ( || {
@@ -198,6 +238,11 @@ unsafe extern "C-unwind" fn swizzled_dragging_entered(this: &AnyObject, cmd: Sel
198238
199239 // Always emit modifiers on enter (initial state for this drag session)
200240 emit_modifiers_forced ( ) ;
241+
242+ // For self-drags, swap the OS drag image to transparent so it's invisible inside the window.
243+ // The rich PNG (set at drag start) remains as the session image shown outside the window.
244+ // The DOM overlay handles the visual feedback inside.
245+ unsafe { drag_image_swap:: on_drag_entered ( drag_info) } ;
201246 } ) ) ;
202247
203248 if result. is_err ( ) {
@@ -236,6 +281,29 @@ unsafe extern "C-unwind" fn swizzled_dragging_updated(this: &AnyObject, cmd: Sel
236281 unsafe { call_original_updated ( this, cmd, drag_info) }
237282}
238283
284+ // --- draggingExited: swizzle ---
285+
286+ unsafe extern "C-unwind" fn swizzled_dragging_exited ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) {
287+ // Swap back to the rich image so it's visible outside the window.
288+ // setDraggingFrame:contents: modifications persist globally, so the transparent image
289+ // from draggingEntered: would remain visible outside without this swap-back.
290+ let result = catch_unwind ( AssertUnwindSafe ( || {
291+ unsafe { drag_image_swap:: on_drag_exited ( drag_info) } ;
292+ } ) ) ;
293+
294+ if result. is_err ( ) {
295+ warn_once (
296+ & WARNED_EXITED_PANIC ,
297+ "drag_image_detection: panic in draggingExited swizzle — drag image swap-back \
298+ to rich preview is disabled for this session. \
299+ Check NSImage and NSDraggingItem usage in drag_image_detection.rs.",
300+ ) ;
301+ }
302+
303+ // Always forward to wry's original implementation.
304+ unsafe { call_original_exited ( this, cmd, drag_info) }
305+ }
306+
239307// --- Drag image size reading ---
240308
241309unsafe fn read_drag_image_size ( drag_info : & AnyObject ) -> ( f64 , f64 ) {
@@ -262,7 +330,7 @@ unsafe fn read_drag_image_size(drag_info: &AnyObject) -> (f64, f64) {
262330
263331 let image: * const AnyObject = unsafe { msg_send ! [ drag_info, draggedImage] } ;
264332 if !image. is_null ( ) {
265- let ns_size: objc2_foundation :: NSSize = unsafe { msg_send ! [ image, size] } ;
333+ let ns_size: NSSize = unsafe { msg_send ! [ image, size] } ;
266334 if ns_size. width > 0.0 || ns_size. height > 0.0 {
267335 return ( ns_size. width , ns_size. height ) ;
268336 }
0 commit comments