//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // The Win32 backend's pointer, capture, close and teardown handling. Posts // a real Window's HWND the messages Windows would send it, pumps them as // StartSync does, and checks what the Window made of them: // // - a move into the client area is an enter, with the entry position // already in currentMousePos, and a move; WM_MOUSELEAVE is one leave // - a position past the client area (a drag under capture) arrives // signed, and is a move only // - a press captures the mouse and its release lets go; losing the // capture mid-drag releases the button, and the button-up after that // fires nothing // - the wheel adds up sub-detent steps, and a leave drops them // - WM_CLOSE fires onClose and clears `open`, but leaves the HWND alone; // ~Window destroys it, what is still queued for it reaches nothing, // and another window can be made after it // - an enter listener that takes the window away stops the move that // would follow // // The window is moved off-screen first, so the real cursor stays out of // it. Built on Linux, it runs under Wine: // crafter-build test --target=x86_64-w64-mingw32 PointerWin32 #include #include import Crafter.Graphics; import Crafter.Event; import std; using namespace Crafter; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // One pass of StartSync's pump. void Pump() { MSG msg; while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessageW(&msg); } } void Post(HWND hwnd, UINT msg, WPARAM wParam = 0, LPARAM lParam = 0) { PostMessage(hwnd, msg, wParam, lParam); Pump(); } // An event as it fired, and what the window looked like then. struct Seen { std::string_view event; float x; float y; bool inside; bool operator==(const Seen&) const = default; }; } // namespace int main() { Device::Initialize(); auto window = std::make_unique(320, 240); const HWND hwnd = window->hwnd; SetWindowPos(hwnd, nullptr, -20000, -20000, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE); // Whatever the real cursor did while the window was on screen. Pump(); Post(hwnd, WM_MOUSELEAVE); std::vector seen; auto record = [&](std::string_view event) { return [&seen, &window, event] { seen.push_back({ event, window->currentMousePos.x, window->currentMousePos.y, window->mouseInside }); }; }; auto count = [&](std::string_view event) { return std::ranges::count(seen, event, &Seen::event); }; EventListener enterSub(&window->onMouseEnter, record("enter")); EventListener moveSub(&window->onMouseMove, record("move")); EventListener leaveSub(&window->onMouseLeave, record("leave")); Post(hwnd, WM_MOUSEMOVE, 0, MAKELPARAM(10, 20)); Check(seen.size() >= 2 && seen[0] == Seen{ "enter", 10.0f, 20.0f, true } && seen[1] == Seen{ "move", 10.0f, 20.0f, true }, "a move into the client area: an enter at the entry position, then a move"); // Windows answers a TME_LEAVE for a window the cursor is not over with // a leave at once, Wine with none, so the leave posted here is the // second on Windows and the first on Wine: either way, one leave. Post(hwnd, WM_MOUSELEAVE); Check(count("leave") == 1 && seen.back() == Seen{ "leave", 10.0f, 20.0f, false }, "WM_MOUSELEAVE: a leave, with mouseInside cleared"); Post(hwnd, WM_MOUSELEAVE); Check(count("leave") == 1, "a second WM_MOUSELEAVE fires nothing"); seen.clear(); Post(hwnd, WM_MOUSEMOVE, MK_LBUTTON, MAKELPARAM(-5, -7)); Check(seen == std::vector{ { "move", -5.0f, -7.0f, false } }, "a position past the client area: a move only, signed"); seen.clear(); int clicks = 0; int releases = 0; EventListener clickSub(&window->onMouseLeftClick, [&] { ++clicks; }); EventListener releaseSub(&window->onMouseLeftRelease, [&] { ++releases; }); Post(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(10, 20)); Check(clicks == 1 && window->mouseLeftHeld && GetCapture() == hwnd, "a press: a click, held, and the mouse captured"); Post(hwnd, WM_LBUTTONUP, 0, MAKELPARAM(10, 20)); Check(releases == 1 && !window->mouseLeftHeld && GetCapture() != hwnd, "its release: a release, and the capture let go"); Post(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(10, 20)); // As when the capture is taken away mid-drag. ReleaseCapture(); Pump(); Check(releases == 2 && !window->mouseLeftHeld, "losing the capture mid-drag releases the button"); Post(hwnd, WM_LBUTTONUP, 0, MAKELPARAM(10, 20)); Check(releases == 2, "and the button-up after that fires nothing"); std::vector scrolls; EventListener scrollSub(&window->onMouseScroll, [&](std::uint32_t delta) { scrolls.push_back(static_cast(delta)); }); auto wheel = [&](int delta) { Post(hwnd, WM_MOUSEWHEEL, MAKEWPARAM(0, delta), 0); }; wheel(-WHEEL_DELTA); Check(scrolls == std::vector{ 1 }, "one detent toward the user: +1"); scrolls.clear(); wheel(WHEEL_DELTA / 2); wheel(WHEEL_DELTA / 2); Check(scrolls == std::vector{ -1 }, "two half detents away from the user add up to -1"); scrolls.clear(); wheel(-WHEEL_DELTA / 2); Post(hwnd, WM_MOUSELEAVE); wheel(-WHEEL_DELTA / 2); Check(scrolls.empty(), "a leave drops a half detent"); wheel(-WHEEL_DELTA / 2); Check(scrolls == std::vector{ 1 }, "after which two add up again"); int closes = 0; EventListener closeSub(&window->onClose, [&] { ++closes; }); Post(hwnd, WM_CLOSE); Check(closes == 1 && !window->open && IsWindow(hwnd), "WM_CLOSE: onClose, open cleared, the HWND still there"); // ~Window detaches the HWND before destroying it, which is how WndProc // tells that a listener destroyed the window before it reports the // move. Destroying it for real here would leave Event::Invoke reading // the destroyed event once the listener returns, so this listener // detaches the HWND itself, as ~Window does. seen.clear(); { EventListener goneSub(&window->onMouseEnter, [hwnd] { SetWindowLongPtr(hwnd, GWLP_USERDATA, 0); }); Post(hwnd, WM_MOUSEMOVE, 0, MAKELPARAM(30, 40)); SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast(window.get())); } Check(seen == std::vector{ { "enter", 30.0f, 40.0f, true } }, "no move after an enter listener takes the window away"); PostMessage(hwnd, WM_MOUSEMOVE, 0, MAKELPARAM(50, 60)); PostMessage(hwnd, WM_MOUSELEAVE, 0, 0); PostMessage(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(50, 60)); seen.clear(); clicks = 0; window.reset(); Check(!IsWindow(hwnd), "~Window destroys the HWND"); Pump(); Check(seen.empty() && clicks == 0, "and what was still queued for it reaches nothing"); // As a resident app does on every hide and show. The window class is // still registered from the first. window = std::make_unique(320, 240); Check(IsWindow(window->hwnd), "another window can be made after it"); if (failures != 0) { std::println("{} check(s) failed", failures); return EXIT_FAILURE; } std::println("all checks passed"); return EXIT_SUCCESS; }