Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/webview2-drag-drop-hidden-window.md
Original file line number Diff line number Diff line change
@@ -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.
108 changes: 77 additions & 31 deletions src/webview2/drag_drop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -35,44 +30,95 @@ use windows::{
},
};

#[derive(Default)]
pub(crate) struct DragDropController {
drop_targets: Vec<IDropTarget>,
// 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<dyn Fn(DragDropEvent) -> bool>,
}

impl DragDropController {
#[inline]
pub(crate) fn new(hwnd: HWND, handler: Box<dyn Fn(DragDropEvent) -> bool>) -> Self {
let mut controller = DragDropController::default();
pub(crate) fn new(parent: HWND, handler: Box<dyn Fn(DragDropEvent) -> 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<dyn Fn(DragDropEvent) -> 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) };
}
}
}

Expand Down
113 changes: 104 additions & 9 deletions src/webview2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32> = Lazy::new(|| unsafe { RegisterWindowMessageA(s!("Wry::ExecMsg")) });

impl From<webview2_com::Error> for Error {
Expand All @@ -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<DragDropController>,
// Wrapped in Rc<RefCell<..>> so the container WndProc can trigger a late `reinit()` via HWND
// user data (no dependency on external callers like Tauri).
drag_drop_controller: Option<Rc<RefCell<DragDropController>>>,
}

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) };
Expand Down Expand Up @@ -153,9 +164,24 @@ impl InnerWebView {
.cast::<ICoreWebView2Controller4>()
.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),
Expand All @@ -182,20 +208,79 @@ impl InnerWebView {
attributes: &WebViewAttributes,
is_child: bool,
) -> Result<HWND> {
#[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<DragDropController>;
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<DragDropController>;
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)
Expand Down Expand Up @@ -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(())
}

Expand Down
Loading