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
8 changes: 7 additions & 1 deletion src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2265,7 +2265,13 @@ unsafe extern "system" fn mirror_wnd_proc(
) -> LRESULT {
match msg {
WM_NCHITTEST => LRESULT(HTCLIENT as isize),
WM_SETCURSOR if set_surface_cursor(hwnd) => LRESULT(1),
// See the primary surface: DefWindowProc forwards both to the parent.
WM_MOUSEACTIVATE => LRESULT(MA_NOACTIVATE as isize),
WM_MOUSEWHEEL | WM_MOUSEHWHEEL | WM_APPCOMMAND | WM_CONTEXTMENU => LRESULT(0),
WM_SETCURSOR => {
set_surface_cursor(hwnd);
LRESULT(1)
}
WM_MOUSEMOVE => {
update_mouse_hover(hwnd, lparam);
LRESULT(0)
Expand Down
101 changes: 99 additions & 2 deletions src/window/message_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ pub(super) unsafe extern "system" fn wnd_proc(
LRESULT(0)
}
WM_NCHITTEST => LRESULT(HTCLIENT as isize),
// DefWindowProc sends WM_MOUSEACTIVATE to a child's parent first. For a
// taskbar-hosted surface that is a synchronous call into Shell_TrayWnd
// on every button press; a hang dump caught Explorer's taskbar thread
// stuck in UI Automation while handling exactly that message (from a
// right-button press), with this thread waiting on it. The surface is
// never activated anyway, so answer here and keep Explorer out of it.
WM_MOUSEACTIVATE => LRESULT(MA_NOACTIVATE as isize),
// Same parent forwarding: the surface has no use for them either.
WM_MOUSEWHEEL | WM_MOUSEHWHEEL | WM_APPCOMMAND | WM_CONTEXTMENU => LRESULT(0),
WM_PAINT => {
let mut ps = PAINTSTRUCT::default();
let _ = BeginPaint(hwnd, &mut ps);
Expand Down Expand Up @@ -187,8 +196,10 @@ pub(super) unsafe extern "system" fn wnd_proc(
schedule_auto_update_check(hwnd);
LRESULT(0)
}
WM_SETCURSOR if set_surface_cursor(hwnd) => LRESULT(1),
WM_SETCURSOR => DefWindowProcW(hwnd, msg, wparam, lparam),
WM_SETCURSOR => {
set_surface_cursor(hwnd);
LRESULT(1)
}
WM_LBUTTONDOWN => {
widget_tooltip::hide();
unsafe {
Expand Down Expand Up @@ -1082,4 +1093,90 @@ mod tests {
worker.join().expect("tray callback test thread");
result.expect("tray callbacks must return without waiting for STATE");
}

#[test]
fn hosted_surface_never_forwards_mouse_messages_to_its_parent() {
// Stands in for Shell_TrayWnd: DefWindowProc of a child forwards these
// to the parent synchronously, which in production is Explorer.
static FORWARDED: AtomicU32 = AtomicU32::new(0);
unsafe extern "system" fn parent_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if matches!(
msg,
WM_MOUSEACTIVATE | WM_SETCURSOR | WM_MOUSEWHEEL | WM_MOUSEHWHEEL | WM_APPCOMMAND
) {
FORWARDED.fetch_add(1, Ordering::SeqCst);
}
DefWindowProcW(hwnd, msg, wparam, lparam)
}

unsafe {
let class = native_interop::wide_str("ClaudeCodeUsageForwardingParentTest");
RegisterClassW(&WNDCLASSW {
lpfnWndProc: Some(parent_proc),
hInstance: GetModuleHandleW(PCWSTR::null()).unwrap().into(),
lpszClassName: PCWSTR(class.as_ptr()),
..Default::default()
});
let parent = CreateWindowExW(
WINDOW_EX_STYLE::default(),
PCWSTR(class.as_ptr()),
PCWSTR::null(),
WS_POPUP,
0,
0,
10,
10,
None,
None,
None,
None,
)
.unwrap();
let child = CreateWindowExW(
WINDOW_EX_STYLE::default(),
windows::core::w!("STATIC"),
PCWSTR::null(),
WS_CHILD,
0,
0,
10,
10,
Some(parent),
None,
None,
None,
)
.unwrap();

// The harness observes forwarding when DefWindowProc handles it.
let press = LPARAM(((WM_RBUTTONDOWN as isize) << 16) | HTCLIENT as isize);
DefWindowProcW(child, WM_MOUSEACTIVATE, WPARAM(parent.0 as usize), press);
assert_eq!(FORWARDED.swap(0, Ordering::SeqCst), 1);

assert_eq!(
wnd_proc(child, WM_MOUSEACTIVATE, WPARAM(parent.0 as usize), press).0,
MA_NOACTIVATE as isize
);
let hover = LPARAM(((WM_MOUSEMOVE as isize) << 16) | HTCLIENT as isize);
assert_eq!(
wnd_proc(child, WM_SETCURSOR, WPARAM(child.0 as usize), hover).0,
1
);
for msg in [WM_MOUSEWHEEL, WM_MOUSEHWHEEL, WM_APPCOMMAND] {
assert_eq!(wnd_proc(child, msg, WPARAM(0), LPARAM(0)).0, 0);
}
let forwarded = FORWARDED.load(Ordering::SeqCst);
let _ = DestroyWindow(child);
let _ = DestroyWindow(parent);
assert_eq!(
forwarded, 0,
"surface forwarded a mouse message to its host"
);
}
}
}
23 changes: 15 additions & 8 deletions src/window/mouse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,19 @@ pub(super) fn clear_tray_mouse_hover_if_left(hwnd: HWND) {
}
}

pub(super) unsafe fn set_surface_cursor(hwnd: HWND) -> bool {
/// Answer WM_SETCURSOR without DefWindowProc. For a child surface it first
/// sends WM_SETCURSOR to the parent, which for a taskbar-hosted surface is a
/// synchronous call into Explorer on every mouse move.
pub(super) unsafe fn set_surface_cursor(hwnd: HWND) {
let cursor = if surface_cursor_is_clickable(hwnd) {
IDC_HAND
} else {
IDC_ARROW
};
SetCursor(Some(LoadCursorW(None, cursor).unwrap_or_default()));
}

unsafe fn surface_cursor_is_clickable(hwnd: HWND) -> bool {
let mut point = POINT::default();
if GetCursorPos(&mut point).is_err() {
return false;
Expand All @@ -359,18 +371,13 @@ pub(super) unsafe fn set_surface_cursor(hwnd: HWND) -> bool {
let Some((surface, object)) = mouse_target_at(hwnd, LPARAM(packed as isize)) else {
return false;
};
let clickable = [
[
MouseEventKind::Click,
MouseEventKind::DoubleClick,
MouseEventKind::RightClick,
]
.into_iter()
.any(|event| mouse_handler_exists(surface, &object, event));
if clickable {
let cursor = LoadCursorW(None, IDC_HAND).unwrap_or_default();
SetCursor(Some(cursor));
}
clickable
.any(|event| mouse_handler_exists(surface, &object, event))
}

#[cfg(test)]
Expand Down
5 changes: 4 additions & 1 deletion src/window/widget_tooltip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@ pub(super) fn show(owner: HWND) {
return;
};
unsafe {
let Ok(window) = create_window(owner) else {
// Owned by the controller, never the surface: a popup owned by a child
// is owned by its top-level ancestor, which for a taskbar-hosted
// surface would put this window in Explorer's Shell_TrayWnd owner chain.
let Ok(window) = create_window(controller) else {
diagnose::log("unable to create widget tooltip");
return;
};
Expand Down
Loading