Skip to content

Commit c344e71

Browse files
committed
Fix window hit testing and late drag starts on Windows
- WindowManager::GetWindowAtPoint() asks WindowFromPoint() first, so click-through overlays (such as the NVIDIA GeForce overlay) no longer hide every window, and only walks the z-order below the excluded window. - Windows: WindowManager::Get() finds windows that carry an ID but were never registered (created by Window() or wrapped by a binding), hidden or untitled ones included. - WindowDragSession no longer moves the window once the primary button is up: a drag recognized after the release ends where the cursor was last followed instead of jumping to where it went since. - window_drag_session_example tears the panel off only once the cursor moves, since focus changes arrive asynchronously on Windows.
1 parent f6ff436 commit c344e71

4 files changed

Lines changed: 113 additions & 55 deletions

File tree

‎examples/window_drag_session_example/main.cpp‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ int main() {
3838
bool over_dock = false;
3939
// Hiding the panel on drop hands focus to the dock; that is not a press.
4040
bool dropping = false;
41+
// Focus changes are not always presses (and arrive late on some platforms),
42+
// so a tear-off only happens once the cursor moves with the button down.
43+
bool tear_off_pending = false;
4144

4245
auto contains = [](const nativeapi::Rectangle& r, const Point& p) {
4346
return p.x >= r.x && p.y >= r.y && p.x < r.x + r.width && p.y < r.y + r.height;
@@ -55,18 +58,25 @@ int main() {
5558
session->Start(panel, {cursor.x - position.x, cursor.y - position.y});
5659
} else if (event.GetWindowId() == dock->GetId() && docked &&
5760
contains(dock->GetContentBounds(), cursor)) {
61+
// Follow the pointer; the panel only comes out once it moves.
62+
tear_off_pending = session->Start(nullptr, {0, 0});
63+
}
64+
});
65+
66+
session->AddListener<WindowDragMovedEvent>([&](const WindowDragMovedEvent& event) {
67+
if (tear_off_pending) {
5868
// Tear off: bring the panel back under the cursor, grabbed by its title bar.
69+
tear_off_pending = false;
70+
Point cursor = event.GetCursorPosition();
5971
Point anchor{130, 12};
6072
panel->SetPosition({cursor.x - anchor.x, cursor.y - anchor.y});
6173
panel->Show();
6274
docked = false;
6375
session->Start(panel, anchor);
6476
dock->SetTitle("Dock - drop the panel here");
6577
std::cout << "Panel torn off" << std::endl;
78+
return;
6679
}
67-
});
68-
69-
session->AddListener<WindowDragMovedEvent>([&](const WindowDragMovedEvent& event) {
7080
auto target = window_manager.GetWindowAtPoint(event.GetCursorPosition(), panel->GetId());
7181
bool now_over_dock = target && target->GetId() == dock->GetId();
7282
if (now_over_dock != over_dock) {
@@ -77,6 +87,10 @@ int main() {
7787
});
7888

7989
session->AddListener<WindowDragEndedEvent>([&](const WindowDragEndedEvent& event) {
90+
if (tear_off_pending) {
91+
tear_off_pending = false; // Released without moving: just a click.
92+
return;
93+
}
8094
Point cursor = event.GetCursorPosition();
8195
std::cout << "Drag ended at (" << cursor.x << ", " << cursor.y << ")" << std::endl;
8296
panel->SetOpacity(1.0f);
@@ -91,8 +105,10 @@ int main() {
91105
}
92106
});
93107

94-
session->AddListener<WindowDragCancelledEvent>(
95-
[&](const WindowDragCancelledEvent&) { std::cout << "Drag cancelled" << std::endl; });
108+
session->AddListener<WindowDragCancelledEvent>([&](const WindowDragCancelledEvent&) {
109+
tear_off_pending = false;
110+
std::cout << "Drag cancelled" << std::endl;
111+
});
96112

97113
std::cout << "Press inside the Panel window and drag it onto the Dock window to dock it."
98114
<< std::endl;

‎src/platform/windows/window_manager_windows.cpp‎

Lines changed: 78 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,16 @@ static WindowId GetWindowIdFromHwnd(HWND hwnd) {
2626
return IdAllocator::kInvalidId;
2727
}
2828

29-
// First, try to get window ID from HWND's custom property
29+
// First, try to get window ID from HWND's custom property. A window can carry
30+
// an ID without being registered (created by Window(), or wrapped by a
31+
// binding), so make sure WindowManager::Get() can find it either way.
3032
HANDLE prop_handle = GetPropW(hwnd, kWindowIdProperty);
3133
if (prop_handle) {
3234
WindowId window_id = static_cast<WindowId>(reinterpret_cast<uintptr_t>(prop_handle));
3335
if (window_id != IdAllocator::kInvalidId && window_id != 0) {
36+
if (!WindowRegistry::GetInstance().Get(window_id)) {
37+
WindowRegistry::GetInstance().Add(window_id, std::make_shared<Window>(hwnd));
38+
}
3439
return window_id;
3540
}
3641
}
@@ -459,18 +464,43 @@ WindowManager::~WindowManager() {
459464
StopEventListening();
460465
}
461466

467+
namespace {
468+
469+
struct WindowIdSearch {
470+
WindowId id;
471+
HWND found;
472+
};
473+
474+
// Finds this process's top-level window carrying an ID, visible or not.
475+
BOOL CALLBACK FindWindowWithId(HWND hwnd, LPARAM data) {
476+
auto* search = reinterpret_cast<WindowIdSearch*>(data);
477+
if (!IsOwnProcessWindow(hwnd)) {
478+
return TRUE;
479+
}
480+
HANDLE prop_handle = GetPropW(hwnd, kWindowIdProperty);
481+
if (prop_handle && static_cast<WindowId>(reinterpret_cast<uintptr_t>(prop_handle)) == search->id) {
482+
search->found = hwnd;
483+
return FALSE;
484+
}
485+
return TRUE;
486+
}
487+
488+
} // namespace
489+
462490
std::shared_ptr<Window> WindowManager::Get(WindowId id) {
463491
// First try to get from registry
464492
auto window = WindowRegistry::GetInstance().Get(id);
465493
if (window) {
466494
return window;
467495
}
468496

469-
// If not in registry, enumerate all windows to find it
470-
// This will create and register the window if it exists
471-
GetAll();
472-
473-
// Try again after enumeration
497+
// Not registered yet: the window may still exist, hidden or untitled (which
498+
// GetAll() skips), so look for the ID on this process's windows directly.
499+
WindowIdSearch search = {id, nullptr};
500+
EnumWindows(FindWindowWithId, reinterpret_cast<LPARAM>(&search));
501+
if (search.found) {
502+
GetWindowIdFromHwnd(search.found);
503+
}
474504
return WindowRegistry::GetInstance().Get(id);
475505
}
476506

@@ -573,41 +603,38 @@ POINT LogicalToPhysicalScreenPoint(Point point) {
573603
static_cast<LONG>(std::lround(point.y * scale))};
574604
}
575605

576-
struct HitTestSearch {
577-
POINT point;
578-
HWND excluded;
579-
HWND found;
580-
};
581-
582-
// EnumWindows visits top-level windows in z-order, topmost first, so the first
583-
// window that is actually painted at the point is the one the user sees there.
584-
BOOL CALLBACK HitTestTopLevelWindow(HWND hwnd, LPARAM data) {
585-
auto* search = reinterpret_cast<HitTestSearch*>(data);
586-
if (hwnd == search->excluded || !IsWindowVisible(hwnd) || IsIconic(hwnd)) {
587-
return TRUE;
606+
// Whether `hwnd`, a top-level window, visibly covers `point`. Used below the
607+
// window excluded from a hit test, where the system's own hit test cannot look.
608+
bool CoversPoint(HWND hwnd, POINT point) {
609+
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
610+
return false;
588611
}
589612
LONG ex_style = GetWindowLongW(hwnd, GWL_EXSTYLE);
590-
if ((ex_style & WS_EX_LAYERED) && (ex_style & WS_EX_TRANSPARENT)) {
591-
return TRUE; // Click-through overlay.
613+
if (ex_style & WS_EX_LAYERED) {
614+
if (ex_style & WS_EX_TRANSPARENT) {
615+
return false; // Click-through overlay.
616+
}
617+
BYTE alpha = 255;
618+
DWORD flags = 0;
619+
if (GetLayeredWindowAttributes(hwnd, nullptr, &alpha, &flags) && (flags & LWA_ALPHA) &&
620+
alpha == 0) {
621+
return false; // Fully transparent.
622+
}
592623
}
593624
BOOL cloaked = FALSE;
594625
if (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked))) &&
595626
cloaked) {
596-
return TRUE; // On another virtual desktop, or a suspended UWP frame.
627+
return false; // On another virtual desktop, or a suspended UWP frame.
597628
}
598629
// The extended frame excludes the invisible resize borders that
599630
// GetWindowRect() includes on Windows 10 and later.
600631
RECT rect;
601632
if (FAILED(DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(rect)))) {
602633
if (!GetWindowRect(hwnd, &rect)) {
603-
return TRUE;
634+
return false;
604635
}
605636
}
606-
if (!PtInRect(&rect, search->point)) {
607-
return TRUE;
608-
}
609-
search->found = hwnd;
610-
return FALSE;
637+
return PtInRect(&rect, point) != FALSE;
611638
}
612639

613640
} // namespace
@@ -619,13 +646,32 @@ std::shared_ptr<Window> WindowManager::GetWindowAtPoint(Point point, WindowId ex
619646
excluded = static_cast<HWND>(window->GetNativeObject());
620647
}
621648
}
622-
623-
HitTestSearch search = {LogicalToPhysicalScreenPoint(point), excluded, nullptr};
624-
EnumWindows(HitTestTopLevelWindow, reinterpret_cast<LPARAM>(&search));
625-
if (!search.found || !IsOwnProcessWindow(search.found)) {
649+
const POINT physical = LogicalToPhysicalScreenPoint(point);
650+
651+
// Ask the system first: WindowFromPoint() is the window a click would reach,
652+
// so it looks through click-through overlays (layered windows with
653+
// transparent pixels, like game or capture overlays) that cover the screen.
654+
HWND found = WindowFromPoint(physical);
655+
if (found) {
656+
found = GetAncestor(found, GA_ROOT);
657+
}
658+
if (found && found == excluded) {
659+
// The excluded window, typically the one being dragged, is right under the
660+
// point. Walk down the stack beneath it instead.
661+
found = nullptr;
662+
for (HWND below = GetWindow(excluded, GW_HWNDNEXT); below;
663+
below = GetWindow(below, GW_HWNDNEXT)) {
664+
if (CoversPoint(below, physical)) {
665+
found = below;
666+
break;
667+
}
668+
}
669+
}
670+
// Another application's window on top means the point is covered.
671+
if (!found || !IsOwnProcessWindow(found)) {
626672
return nullptr;
627673
}
628-
WindowId window_id = GetWindowIdFromHwnd(search.found);
674+
WindowId window_id = GetWindowIdFromHwnd(found);
629675
if (window_id == IdAllocator::kInvalidId) {
630676
return nullptr;
631677
}

‎src/window_drag_session.cpp‎

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ bool WindowDragSession::Start(std::shared_ptr<Window> window, Point anchor) {
1717
anchor_ = anchor;
1818
last_cursor_position_ = cursor_position;
1919
active_ = true;
20-
++generation_;
2120

22-
if (window_) {
21+
// A gesture can be recognized after the button was already released; the
22+
// first tick reports the end then, and the window must not jump to wherever
23+
// the cursor has gone meanwhile.
24+
if (window_ && primary_button_down) {
2325
MoveWindow(*window_, cursor_position);
2426
}
2527
StartTicking();
@@ -67,24 +69,21 @@ void WindowDragSession::HandleTick() {
6769
return;
6870
}
6971

72+
// Released: the drag is over where the cursor was last followed. Moving now
73+
// would carry the window to wherever the cursor went after the release.
74+
if (!primary_button_down) {
75+
Finish(false, cursor_position);
76+
return;
77+
}
78+
7079
const bool moved = std::fabs(cursor_position.x - last_cursor_position_.x) >= 0.5 ||
7180
std::fabs(cursor_position.y - last_cursor_position_.y) >= 0.5;
7281
if (moved) {
7382
last_cursor_position_ = cursor_position;
7483
if (window_) {
7584
MoveWindow(*window_, cursor_position);
7685
}
77-
const unsigned long generation = generation_;
7886
Emit<WindowDragMovedEvent>(window_id_, cursor_position);
79-
// A listener restarted or stopped the session; the release below, if any,
80-
// belongs to whatever state it left behind and is picked up next tick.
81-
if (generation != generation_ || !active_) {
82-
return;
83-
}
84-
}
85-
86-
if (!primary_button_down) {
87-
Finish(false, cursor_position);
8887
}
8988
}
9089

@@ -93,7 +92,6 @@ void WindowDragSession::Finish(bool cancelled, Point cursor_position) {
9392
return;
9493
}
9594
active_ = false;
96-
++generation_;
9795
StopTicking();
9896

9997
const WindowId window_id = window_id_;

‎src/window_drag_session.h‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ class WindowDragSession : public EventEmitter<WindowDragEvent> {
127127
* point inside the content.
128128
* @return false if the platform cannot track the cursor globally.
129129
*
130-
* If the primary button is not held down when the session first samples the
131-
* pointer, the session ends right away with a WindowDragEndedEvent.
130+
* If the primary button is not held down when the session starts, the window
131+
* is left where it is and the session ends right away with a
132+
* WindowDragEndedEvent.
132133
*
133134
* Calling Start() on an active session retargets it: the previous target is
134135
* released without a WindowDragEndedEvent or WindowDragCancelledEvent, so the
@@ -182,9 +183,6 @@ class WindowDragSession : public EventEmitter<WindowDragEvent> {
182183
Point anchor_{0, 0};
183184
Point last_cursor_position_{0, 0};
184185
bool active_ = false;
185-
// Incremented by every Start()/Finish() so a tick can tell whether a
186-
// listener it called restarted or stopped the session.
187-
unsigned long generation_ = 0;
188186
};
189187

190188
} // namespace nativeapi

0 commit comments

Comments
 (0)