77//! Events emitted:
88//! - `drag-image-size` `{ width, height }` — on drag enter
99//! - `drag-modifiers` `{ altHeld }` — on drag enter and every drag update (only when changed)
10+ //!
11+ //! ## Resilience
12+ //!
13+ //! All native API calls are guarded against class/method removal. If wry renames its
14+ //! internal webview class or macOS deprecates APIs we rely on, the swizzle degrades gracefully:
15+ //! - Drag image detection disabled → the DOM overlay is always shown (redundant but functional)
16+ //! - Modifier key detection disabled → falls back to JS keydown/keyup (works when webview has focus)
17+ //!
18+ //! Rust panics inside swizzled functions are caught via `catch_unwind` to prevent crashes
19+ //! across the FFI boundary. Warning messages include actionable guidance for future maintainers.
1020
21+ use std:: panic:: { AssertUnwindSafe , catch_unwind} ;
1122use std:: ptr:: NonNull ;
1223use std:: sync:: OnceLock ;
1324use std:: sync:: atomic:: { AtomicBool , Ordering } ;
@@ -41,13 +52,33 @@ static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
4152/// Tracks previous alt state so we only emit `drag-modifiers` when it changes.
4253static LAST_ALT_HELD : AtomicBool = AtomicBool :: new ( false ) ;
4354
55+ // Warn-once flags to prevent log spam for issues that recur on every drag event.
56+ static WARNED_NSEVENT_MISSING : AtomicBool = AtomicBool :: new ( false ) ;
57+ static WARNED_ENTERED_PANIC : AtomicBool = AtomicBool :: new ( false ) ;
58+ static WARNED_UPDATED_PANIC : AtomicBool = AtomicBool :: new ( false ) ;
59+ static WARNED_NSARRAY_MISSING : AtomicBool = AtomicBool :: new ( false ) ;
60+ static WARNED_DRAGGED_IMAGE_REMOVED : AtomicBool = AtomicBool :: new ( false ) ;
61+
62+ /// Logs a warning message at most once per app session.
63+ fn warn_once ( flag : & AtomicBool , msg : & str ) {
64+ if !flag. swap ( true , Ordering :: Relaxed ) {
65+ log:: warn!( "{msg}" ) ;
66+ }
67+ }
68+
4469/// Installs swizzles on WryWebView. Call once during app setup.
4570pub fn install ( app_handle : AppHandle ) {
4671 APP_HANDLE . set ( app_handle) . ok ( ) ;
4772
4873 unsafe {
4974 let Some ( cls) = AnyClass :: get ( c"WryWebView" ) else {
50- log:: warn!( "drag_image_detection: WryWebView class not found, skipping swizzle" ) ;
75+ log:: warn!(
76+ "drag_image_detection: WryWebView class not found — swizzle skipped. \
77+ Drag image detection and modifier tracking during drags are disabled. \
78+ This is likely caused by a wry update that renamed the webview class; \
79+ search wry's source for the ObjC class name and update the c\" WryWebView\" \
80+ lookup in drag_image_detection.rs."
81+ ) ;
5182 return ;
5283 } ;
5384
@@ -58,7 +89,12 @@ pub fn install(app_handle: AppHandle) {
5889 swizzled_dragging_entered as * const ( ) ,
5990 ) ) ;
6091 } else {
61- log:: warn!( "drag_image_detection: draggingEntered: not found on WryWebView" ) ;
92+ log:: warn!(
93+ "drag_image_detection: draggingEntered: not found on WryWebView — \
94+ drag image size detection is disabled. \
95+ Wry may have changed how it implements NSDraggingDestination; \
96+ check wry's drag-and-drop event handling in its ObjC layer."
97+ ) ;
6298 }
6399
64100 // Swizzle draggingUpdated:
@@ -68,7 +104,12 @@ pub fn install(app_handle: AppHandle) {
68104 swizzled_dragging_updated as * const ( ) ,
69105 ) ) ;
70106 } else {
71- log:: warn!( "drag_image_detection: draggingUpdated: not found on WryWebView" ) ;
107+ log:: warn!(
108+ "drag_image_detection: draggingUpdated: not found on WryWebView — \
109+ live modifier key tracking during drags is disabled. \
110+ Wry may have changed how it implements NSDraggingDestination; \
111+ check wry's drag-and-drop event handling in its ObjC layer."
112+ ) ;
72113 }
73114
74115 log:: info!( "drag_image_detection: swizzles installed on WryWebView" ) ;
@@ -77,8 +118,19 @@ pub fn install(app_handle: AppHandle) {
77118
78119/// Reads the current Option/Alt key state from `[NSEvent modifierFlags]`.
79120/// This is a class method that reads hardware state — works even when the webview isn't focused.
121+ /// Returns `false` if NSEvent can't be found (graceful degradation).
80122fn is_option_held ( ) -> bool {
81- let flags: usize = unsafe { msg_send ! [ AnyClass :: get( c"NSEvent" ) . unwrap( ) , modifierFlags] } ;
123+ let Some ( cls) = AnyClass :: get ( c"NSEvent" ) else {
124+ warn_once (
125+ & WARNED_NSEVENT_MISSING ,
126+ "drag_image_detection: NSEvent class not found — Alt/Option detection during drags \
127+ is disabled. This is a core AppKit class and shouldn't disappear; if it did, check \
128+ whether macOS moved it to a different framework or renamed it. Modifier detection \
129+ falls back to JS keydown/keyup, which doesn't work during OS-level drags.",
130+ ) ;
131+ return false ;
132+ } ;
133+ let flags: usize = unsafe { msg_send ! [ cls, modifierFlags] } ;
82134 flags & NS_EVENT_MODIFIER_FLAG_OPTION != 0
83135}
84136
@@ -102,48 +154,86 @@ fn emit_modifiers_forced() {
102154 }
103155}
104156
157+ // --- Helpers for forwarding to original implementations ---
158+
159+ /// Forwards to wry's original `draggingEntered:`. Always safe to call — returns
160+ /// `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
167+ }
168+ } }
169+
170+ /// 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
177+ }
178+ } }
179+
105180// --- draggingEntered: swizzle ---
106181
107182unsafe extern "C-unwind" fn swizzled_dragging_entered ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) -> usize {
108- let size = unsafe { read_drag_image_size ( drag_info) } ;
183+ // Our custom logic: read drag image size and emit modifier events.
184+ // Wrapped in catch_unwind to prevent any unexpected Rust panic from crossing the FFI boundary
185+ // and crashing the app mid-drag.
186+ let result = catch_unwind ( AssertUnwindSafe ( || {
187+ let size = unsafe { read_drag_image_size ( drag_info) } ;
188+
189+ if let Some ( app_handle) = APP_HANDLE . get ( ) {
190+ let _ = app_handle. emit (
191+ "drag-image-size" ,
192+ DragImageSize {
193+ width : size. 0 ,
194+ height : size. 1 ,
195+ } ,
196+ ) ;
197+ }
109198
110- if let Some ( app_handle) = APP_HANDLE . get ( ) {
111- let _ = app_handle. emit (
112- "drag-image-size" ,
113- DragImageSize {
114- width : size. 0 ,
115- height : size. 1 ,
116- } ,
199+ // Always emit modifiers on enter (initial state for this drag session)
200+ emit_modifiers_forced ( ) ;
201+ } ) ) ;
202+
203+ if result. is_err ( ) {
204+ warn_once (
205+ & WARNED_ENTERED_PANIC ,
206+ "drag_image_detection: panic in draggingEntered swizzle — drag image detection \
207+ and initial modifier state may not work for this session. \
208+ This is likely caused by a wry or macOS API change; check that NSDraggingItem's \
209+ draggingFrame() and NSEvent's modifierFlags still match the expected signatures \
210+ in drag_image_detection.rs.",
117211 ) ;
118212 }
119213
120- // Always emit modifiers on enter (initial state for this drag session)
121- emit_modifiers_forced ( ) ;
122-
123- if let Some ( & original) = ORIGINAL_ENTERED_IMP . get ( ) {
124- let f = unsafe {
125- std:: mem:: transmute :: < Imp , unsafe extern "C-unwind" fn ( & AnyObject , Sel , & AnyObject ) -> usize > ( original)
126- } ;
127- unsafe { f ( this, cmd, drag_info) }
128- } else {
129- NSDragOperation :: Copy . 0
130- }
214+ // Always forward to wry's original implementation, even if our logic failed.
215+ unsafe { call_original_entered ( this, cmd, drag_info) }
131216}
132217
133218// --- draggingUpdated: swizzle ---
134219
135220unsafe extern "C-unwind" fn swizzled_dragging_updated ( this : & AnyObject , cmd : Sel , drag_info : & AnyObject ) -> usize {
136221 // Only emit when modifier state changes (avoids flooding on every mouse move)
137- emit_modifiers_if_changed ( ) ;
138-
139- if let Some ( & original) = ORIGINAL_UPDATED_IMP . get ( ) {
140- let f = unsafe {
141- std:: mem:: transmute :: < Imp , unsafe extern "C-unwind" fn ( & AnyObject , Sel , & AnyObject ) -> usize > ( original)
142- } ;
143- unsafe { f ( this, cmd, drag_info) }
144- } else {
145- NSDragOperation :: Copy . 0
222+ let result = catch_unwind ( AssertUnwindSafe ( || {
223+ emit_modifiers_if_changed ( ) ;
224+ } ) ) ;
225+
226+ if result. is_err ( ) {
227+ warn_once (
228+ & WARNED_UPDATED_PANIC ,
229+ "drag_image_detection: panic in draggingUpdated swizzle — live modifier key \
230+ tracking during drags is disabled for this session. \
231+ Check NSEvent.modifierFlags usage in drag_image_detection.rs.",
232+ ) ;
146233 }
234+
235+ // Always forward to wry's original implementation.
236+ unsafe { call_original_updated ( this, cmd, drag_info) }
147237}
148238
149239// --- Drag image size reading ---
@@ -154,7 +244,22 @@ unsafe fn read_drag_image_size(drag_info: &AnyObject) -> (f64, f64) {
154244 return size;
155245 }
156246
157- // Fallback: try the deprecated draggedImage() — works for same-process drags
247+ // Fallback: try the deprecated `draggedImage()` — works for same-process drags.
248+ // Guard with respondsToSelector: since Apple may remove this deprecated API entirely.
249+ let responds: Bool = unsafe { msg_send ! [ drag_info, respondsToSelector: sel!( draggedImage) ] } ;
250+ if !responds. as_bool ( ) {
251+ warn_once (
252+ & WARNED_DRAGGED_IMAGE_REMOVED ,
253+ "drag_image_detection: draggedImage selector no longer exists on NSDraggingInfo — \
254+ Apple removed this deprecated API in this macOS version. \
255+ The primary path (enumerateDraggingItems) still works; this only affects \
256+ same-process drag size detection as a fallback. \
257+ Remove the draggedImage fallback from read_drag_image_size() in \
258+ drag_image_detection.rs.",
259+ ) ;
260+ return ( 0.0 , 0.0 ) ;
261+ }
262+
158263 let image: * const AnyObject = unsafe { msg_send ! [ drag_info, draggedImage] } ;
159264 if !image. is_null ( ) {
160265 let ns_size: objc2_foundation:: NSSize = unsafe { msg_send ! [ image, size] } ;
@@ -172,7 +277,15 @@ unsafe fn enumerate_dragging_frames(drag_info: &AnyObject) -> (f64, f64) {
172277 let Some ( nsurl_cls) = AnyClass :: get ( c"NSURL" ) else {
173278 return ( 0.0 , 0.0 ) ;
174279 } ;
175- let nsarray_cls = AnyClass :: get ( c"NSArray" ) . expect ( "NSArray class must exist" ) ;
280+ let Some ( nsarray_cls) = AnyClass :: get ( c"NSArray" ) else {
281+ warn_once (
282+ & WARNED_NSARRAY_MISSING ,
283+ "drag_image_detection: NSArray class not found — drag frame enumeration is disabled. \
284+ NSArray is a core Foundation class; if it's missing, something is fundamentally \
285+ wrong with the ObjC runtime. Check if Foundation is loaded correctly.",
286+ ) ;
287+ return ( 0.0 , 0.0 ) ;
288+ } ;
176289 let class_array: * const AnyObject =
177290 unsafe { msg_send ! [ nsarray_cls, arrayWithObject: nsurl_cls as * const AnyClass ] } ;
178291 if class_array. is_null ( ) {
0 commit comments