From 90c424e81391f94d7acc76b5c32e8ac2879aa914 Mon Sep 17 00:00:00 2001 From: sht2017 <29155206+sht2017@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:37:25 -0700 Subject: [PATCH] fix(windows): enable drag&drop for initially hidden windows (fix tauri#14643) --- .changes/webview2-drag-drop-hidden-window.md | 5 + src/webview2/drag_drop.rs | 108 +++++++++++++----- src/webview2/mod.rs | 113 +++++++++++++++++-- 3 files changed, 186 insertions(+), 40 deletions(-) create mode 100644 .changes/webview2-drag-drop-hidden-window.md diff --git a/.changes/webview2-drag-drop-hidden-window.md b/.changes/webview2-drag-drop-hidden-window.md new file mode 100644 index 000000000..68d9c5526 --- /dev/null +++ b/.changes/webview2-drag-drop-hidden-window.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Fix WebView2 drag and drop for windows created hidden (visible=false) by re-registering drop targets after the window is shown/resized. diff --git a/src/webview2/drag_drop.rs b/src/webview2/drag_drop.rs index 960a07f4d..15b8fbd4b 100644 --- a/src/webview2/drag_drop.rs +++ b/src/webview2/drag_drop.rs @@ -7,18 +7,13 @@ use crate::DragDropEvent; use std::{ - cell::UnsafeCell, - ffi::OsString, - os::{raw::c_void, windows::ffi::OsStringExt}, - path::PathBuf, - ptr, - rc::Rc, + cell::UnsafeCell, ffi::OsString, os::windows::ffi::OsStringExt, path::PathBuf, ptr, rc::Rc, }; use windows::{ core::{implement, BOOL}, Win32::{ - Foundation::{DRAGDROP_E_INVALIDHWND, HWND, LPARAM, POINT, POINTL}, + Foundation::{HWND, LPARAM, POINT, POINTL}, Graphics::Gdi::ScreenToClient, System::{ Com::{IDataObject, DVASPECT_CONTENT, FORMATETC, TYMED_HGLOBAL}, @@ -35,44 +30,95 @@ use windows::{ }, }; -#[derive(Default)] pub(crate) struct DragDropController { - drop_targets: Vec, + // Keep (HWND, IDropTarget) pairs so we can reliably revoke the registration per HWND. + drop_targets: Vec<(HWND, IDropTarget)>, + + // The container HWND that owns the WebView2 child windows. + parent: HWND, + + // Shared handler so each injected IDropTarget can call back without borrowing `self`. + handler: Rc bool>, } impl DragDropController { #[inline] - pub(crate) fn new(hwnd: HWND, handler: Box bool>) -> Self { - let mut controller = DragDropController::default(); + pub(crate) fn new(parent: HWND, handler: Box bool>) -> Self { + let mut controller = DragDropController { + drop_targets: Vec::new(), + parent, + handler: Rc::new(handler), + }; - let handler = Rc::new(handler); + // WebView2's internal child HWNDs may not be stable until after show/resize, but we can + // opportunistically register now and later call `reinit()` when the window actually shows. + controller.register_targets(); + controller + } - // Enumerate child windows to find the WebView2 "window" and override! - { - let mut callback = |hwnd| controller.inject_in_hwnd(hwnd, handler.clone()); - let mut trait_obj: &mut dyn FnMut(HWND) -> bool = &mut callback; - let closure_pointer_pointer: *mut c_void = unsafe { std::mem::transmute(&mut trait_obj) }; - let lparam = LPARAM(closure_pointer_pointer as _); - unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL { - let closure = &mut *(lparam.0 as *mut c_void as *mut &mut dyn FnMut(HWND) -> bool); - closure(hwnd).into() - } - let _ = unsafe { EnumChildWindows(Some(hwnd), Some(enumerate_callback), lparam) }; + #[inline] + pub(crate) fn reinit(&mut self) { + // WebView2 can recreate/replace its internal child HWNDs; revoke and re-enumerate to keep + // the drop target registered on the current live windows. + for (hwnd, _) in self.drop_targets.drain(..) { + let _ = unsafe { RevokeDragDrop(hwnd) }; } - controller + self.register_targets(); } #[inline] - fn inject_in_hwnd(&mut self, hwnd: HWND, handler: Rc bool>) -> bool { - let drag_drop_target: IDropTarget = DragDropTarget::new(hwnd, handler).into(); - if unsafe { RevokeDragDrop(hwnd) } != Err(DRAGDROP_E_INVALIDHWND.into()) - && unsafe { RegisterDragDrop(hwnd, &drag_drop_target) }.is_ok() - { - self.drop_targets.push(drag_drop_target); + pub(crate) fn is_inited(&self) -> bool { + !self.drop_targets.is_empty() + } + + #[inline] + fn register_targets(&mut self) { + // EnumChildWindows requires a C callback; pass `self` through LPARAM. + // Safety: EnumChildWindows is synchronous, so `self` stays valid for the duration. + let this = self as *mut DragDropController; + let lparam = LPARAM(this as isize); + + unsafe extern "system" fn enumerate_callback(child: HWND, lparam: LPARAM) -> BOOL { + let controller = &mut *(lparam.0 as *mut DragDropController); + controller.inject_in_hwnd(child); + true.into() } - true + let ok = unsafe { EnumChildWindows(Some(self.parent), Some(enumerate_callback), lparam) }; + if !ok.as_bool() { + #[cfg(feature = "tracing")] + tracing::debug!("EnumChildWindows failed for parent {:?}", self.parent); + } + } + + #[inline] + fn inject_in_hwnd(&mut self, hwnd: HWND) -> bool { + // Avoid double-registering the same HWND. + if self.drop_targets.iter().any(|(h, _)| *h == hwnd) { + return true; + } + + let handler = self.handler.clone(); + let target: IDropTarget = DragDropTarget::new(hwnd, handler).into(); + + // Override any existing drop target on that HWND (if present), then register ours. + let _ = unsafe { RevokeDragDrop(hwnd) }; + if unsafe { RegisterDragDrop(hwnd, &target) }.is_ok() { + self.drop_targets.push((hwnd, target)); + true + } else { + false + } + } +} + +impl Drop for DragDropController { + fn drop(&mut self) { + // Ensure we don't leave HWNDs registered after the webview/controller is dropped. + for (hwnd, _) in self.drop_targets.drain(..) { + let _ = unsafe { RevokeDragDrop(hwnd) }; + } } } diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs index a2f5d5b99..c7b964258 100644 --- a/src/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -38,6 +38,9 @@ type EventRegistrationToken = i64; const PARENT_SUBCLASS_ID: u32 = WM_USER + 0x64; const PARENT_DESTROY_MESSAGE: u32 = WM_USER + 0x65; const MAIN_THREAD_DISPATCHER_SUBCLASS_ID: u32 = WM_USER + 0x66; +// Private message used to trigger a late drag&drop (re)registration after the HWND is created. +// This avoids relying on WM_SHOWWINDOW/WM_SIZE timing (which can happen before we set GWLP_USERDATA). +const REINIT_DRAG_DROP_MESSAGE: u32 = WM_APP + 0x2A7; static EXEC_MSG_ID: Lazy = Lazy::new(|| unsafe { RegisterWindowMessageA(s!("Wry::ExecMsg")) }); impl From for Error { @@ -63,11 +66,19 @@ pub(crate) struct InnerWebView { // Store FileDropController in here to make sure it gets dropped when // the webview gets dropped, otherwise we'll have a memory leak #[allow(dead_code)] - drag_drop_controller: Option, + // Wrapped in Rc> so the container WndProc can trigger a late `reinit()` via HWND + // user data (no dependency on external callers like Tauri). + drag_drop_controller: Option>>, } impl Drop for InnerWebView { fn drop(&mut self) { + unsafe { + // We store a raw pointer in GWLP_USERDATA; clear it on drop to avoid WndProc dereferencing + // a dangling pointer if the window outlives `InnerWebView`. + let _ = SetWindowLongPtrW(self.hwnd, GWLP_USERDATA, 0); + } + let _ = unsafe { self.controller.Close() }; if self.is_child { let _ = unsafe { DestroyWindow(self.hwnd) }; @@ -153,9 +164,24 @@ impl InnerWebView { .cast::() .and_then(|c| c.SetAllowExternalDrop(false)); } - DragDropController::new(hwnd, handler) + // Allocate on the heap and keep it alive for the lifetime of the webview. + Rc::new(RefCell::new(DragDropController::new(hwnd, handler))) }); + if let Some(dd) = &drag_drop_controller { + // Expose a stable address to the container WndProc so it can reinit on WM_SHOWWINDOW/WM_SIZE. + // Intentionally does NOT bump the refcount: lifetime is managed by `InnerWebView`. + unsafe { + let _ = SetWindowLongPtrW(hwnd, GWLP_USERDATA, Rc::as_ptr(dd) as isize); + } + + // Ensure we attempt at least one late reinit after GWLP_USERDATA is set. + // On Windows, WM_SHOWWINDOW/WM_SIZE can be delivered during CreateWindowExW, before we get here. + unsafe { + let _ = PostMessageW(Some(hwnd), REINIT_DRAG_DROP_MESSAGE, WPARAM(0), LPARAM(0)); + } + } + let w = Self { id, parent: RefCell::new(parent), @@ -182,20 +208,79 @@ impl InnerWebView { attributes: &WebViewAttributes, is_child: bool, ) -> Result { + #[inline] + unsafe fn try_reinit_drag_drop_from_userdata(hwnd: HWND) { + // Fetch the controller pointer previously stored in GWLP_USERDATA. + let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const RefCell; + if ptr.is_null() { + return; + } + + let dd = &*ptr; + // WndProc can be re-entrant; avoid panicking on RefCell borrow violations. + let should_reinit = dd.try_borrow().map(|dd| !dd.is_inited()).unwrap_or(false); + + if should_reinit { + if let Ok(mut dd) = dd.try_borrow_mut() { + dd.reinit(); + } + } + } + + #[inline] + unsafe fn force_reinit_drag_drop_from_userdata(hwnd: HWND) { + // Same as `try_reinit_*`, but intentionally bypasses `is_inited()`. + // WebView2 can create/replace its child HWNDs after the container is shown. + let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const RefCell; + if ptr.is_null() { + return; + } + + let dd = &*ptr; + if let Ok(mut dd) = dd.try_borrow_mut() { + dd.reinit(); + } + } + unsafe extern "system" fn default_window_proc( hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { - if msg == WM_SETFOCUS { - // Fix https://github.com/DioxusLabs/dioxus/issues/2900 - // Get the first child window of the window - let child = GetWindow(hwnd, GW_CHILD).ok(); - if child.is_some() { - // Set focus to the child window(WebView document) - let _ = SetFocus(child); + match msg { + WM_SETFOCUS => { + // Fix https://github.com/DioxusLabs/dioxus/issues/2900 + // Get the first child window of the window + let child = GetWindow(hwnd, GW_CHILD).ok(); + if child.is_some() { + // Set focus to the child window(WebView document) + let _ = SetFocus(child); + } } + + // When the container window becomes visible, re-register drop target. + // This makes drag&drop work even if the embedding framework never calls `webview.set_visible(true)`. + WM_SHOWWINDOW => { + if wparam.0 != 0 { + // Force one reinit when the container becomes visible to catch HWNDs that are + // created/replaced by WebView2 during first show. + unsafe { force_reinit_drag_drop_from_userdata(hwnd) }; + } + } + + // Late initialization hook after `GWLP_USERDATA` is populated. + REINIT_DRAG_DROP_MESSAGE => unsafe { force_reinit_drag_drop_from_userdata(hwnd) }, + + // Often the WebView2 child is only ready after the first layout/resize. + WM_SIZE => unsafe { try_reinit_drag_drop_from_userdata(hwnd) }, + + // Extra safety: clear user data when the HWND is going away. + WM_NCDESTROY => { + let _ = unsafe { SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0) }; + } + + _ => {} } DefWindowProcW(hwnd, msg, wparam, lparam) @@ -1470,6 +1555,16 @@ impl InnerWebView { self.controller.SetIsVisible(visible)?; } + if visible { + if let Some(dd) = &self.drag_drop_controller { + // If the controller was created before WebView2's internal child HWNDs were ready, + // make sure we (re)register the drop targets when the user explicitly shows it. + if !dd.borrow().is_inited() { + dd.borrow_mut().reinit(); + } + } + } + Ok(()) }