Win32
- WM_MOUSEMOVE is handled again: c9ebd44 put the WM_CHAR case where it
was, so currentMousePos stopped following the mouse and onMouseMove
never fired. Positions are read signed (GET_X_LPARAM), since under
capture they run past the client area.
- onMouseEnter and onMouseLeave. The first move over the client area
after a leave is the enter, and arms the TME_LEAVE whose WM_MOUSELEAVE
is the leave. An enter counts only once that is armed, a leave only
after an enter, and a move past the client area is a move only.
- A press captures the mouse, so a drag that leaves the window still
sends its moves and its release, as Wayland's implicit grab does.
Losing the capture releases what is held, and a button-up without its
press fires nothing.
- WM_CLOSE fires onClose and clears open, as xdg_toplevel.close does,
instead of destroying the HWND under the swapchain. WM_DESTROY no
longer posts WM_QUIT, which nothing read.
- ~Window detaches the HWND and destroys it. It stayed on screen, and its
messages went to the freed Window. WndProc hands messages without a
Window to DefWindowProc, and looks the Window up again after an event
that more work follows, in case a listener destroyed it.
- The constructor's message pump leaves input queued for StartSync: the
WM_MOUSEMOVE sent when a window appears under the cursor made an
onMouseEnter nobody could hear yet. The mouse state is reset before
the platform code, no longer after it.
- A window after the first finds its class registered, which showed an
error box.
- The wheel remainder is the window's, no longer a function static that
every window shared, and a leave drops it.
Window
- PointerEnter, PointerMove, PointerLeave and PointerScroll hold the rules
both native backends report the pointer by. An enter puts the entry
position in currentMousePos before onMouseEnter fires, and is followed
by a move, on Wayland too now. scrollRemainder replaces
Device::scrollDetentRemainder, and mouseInside is shared.
Tests
- PointerEnterLeave: enter, move and leave through the wl_pointer
callbacks.
- PointerWin32: posts a real HWND the messages above and pumps them as
StartSync does. Built for x86_64-w64-mingw32 it runs under Wine, and is
skipped where Wine is not on PATH.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
180 lines
7.5 KiB
C++
180 lines
7.5 KiB
C++
//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 <windows.h>
|
|
#include <cstdlib>
|
|
|
|
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 (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessage(&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<Window>(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> 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<void> enterSub(&window->onMouseEnter, record("enter"));
|
|
EventListener<void> moveSub(&window->onMouseMove, record("move"));
|
|
EventListener<void> 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<Seen>{ { "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<void> clickSub(&window->onMouseLeftClick, [&] { ++clicks; });
|
|
EventListener<void> 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<std::int32_t> scrolls;
|
|
EventListener<std::uint32_t> scrollSub(&window->onMouseScroll, [&](std::uint32_t delta) {
|
|
scrolls.push_back(static_cast<std::int32_t>(delta));
|
|
});
|
|
auto wheel = [&](int delta) { Post(hwnd, WM_MOUSEWHEEL, MAKEWPARAM(0, delta), 0); };
|
|
wheel(-WHEEL_DELTA);
|
|
Check(scrolls == std::vector<std::int32_t>{ 1 }, "one detent toward the user: +1");
|
|
scrolls.clear();
|
|
wheel(WHEEL_DELTA / 2);
|
|
wheel(WHEEL_DELTA / 2);
|
|
Check(scrolls == std::vector<std::int32_t>{ -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<std::int32_t>{ 1 }, "after which two add up again");
|
|
|
|
int closes = 0;
|
|
EventListener<void> 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<void> goneSub(&window->onMouseEnter, [hwnd] { SetWindowLongPtr(hwnd, GWLP_USERDATA, 0); });
|
|
Post(hwnd, WM_MOUSEMOVE, 0, MAKELPARAM(30, 40));
|
|
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(window.get()));
|
|
}
|
|
Check(seen == std::vector<Seen>{ { "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<Window>(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;
|
|
}
|