Crafter.Graphics/tests/PointerEnterLeave/main.cpp

122 lines
4.5 KiB
C++
Raw Normal View History

fix(win32): restore mouse moves, add enter and leave, capture drags, and close and destroy windows cleanly 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>
2026-10-31 03:29:50 +01:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The pointer's enter, move and leave as the Wayland backend reports them,
// through Window::PointerEnter/PointerMove/PointerLeave — the helpers the
// Win32 backend reports through as well. Drives the wl_pointer listener
// callbacks directly, as MouseScroll does, so no compositor is needed, and
// asserts:
//
// - onMouseEnter finds the entry position (in physical pixels) in
// currentMousePos, and mouseInside set
// - an enter is also a move: onMouseMove follows it, at the entry position
// - onMouseLeave finds mouseInside cleared, and fires once per enter: a
// second leave (sway, on input-device hot-plug) and a leave without an
// enter fire nothing
// - an enter listener that takes the window away stops the move that
// would follow
//
// The Win32 WM_MOUSEMOVE/WM_MOUSELEAVE path calls the same helpers but
// can't be exercised from a Linux test runner.
#include <wayland-client.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;
}
// A default-constructed window has no wl_surface, so a null surface is the
// one PointerListenerHandleEnter finds it by.
void Enter(double x, double y) {
Device::PointerListenerHandleEnter(nullptr, nullptr, /*serial=*/1, /*surface=*/nullptr,
wl_fixed_from_double(x), wl_fixed_from_double(y));
}
void Motion(double x, double y) {
Device::PointerListenerHandleMotion(nullptr, nullptr, /*time=*/0,
wl_fixed_from_double(x), wl_fixed_from_double(y));
}
void Leave() {
Device::PointerListenerHandleLeave(nullptr, nullptr, /*serial=*/2, nullptr);
}
// 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() {
Window window; // default ctor creates no surface — safe without a compositor
window.scale = 2.0f;
Device::windows.push_back(&window);
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 });
};
};
EventListener<void> enterSub(&window.onMouseEnter, record("enter"));
EventListener<void> moveSub(&window.onMouseMove, record("move"));
EventListener<void> leaveSub(&window.onMouseLeave, record("leave"));
Enter(10.5, 20.25);
Check(seen == std::vector<Seen>{ { "enter", 21.0f, 40.5f, true }, { "move", 21.0f, 40.5f, true } },
"enter: entry position in currentMousePos, then a move to it");
Check(Device::focusedWindow == &window, "enter focuses the window");
seen.clear();
Motion(30.0, 5.0);
Check(seen == std::vector<Seen>{ { "move", 60.0f, 10.0f, true } }, "motion: a move");
seen.clear();
Enter(1.0, 1.0);
Check(seen == std::vector<Seen>{ { "move", 2.0f, 2.0f, true } }, "a second enter without a leave is only a move");
seen.clear();
Leave();
Check(seen == std::vector<Seen>{ { "leave", 2.0f, 2.0f, false } }, "leave: mouseInside cleared before onMouseLeave");
Check(Device::focusedWindow == nullptr, "leave clears the focused window");
seen.clear();
Leave();
Device::focusedWindow = &window; // as touch_down sets it, with no enter
Leave();
Check(seen.empty(), "a second leave, and a leave without an enter, fire nothing");
// ~Window clears focusedWindow, which is how the enter handler 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 clears
// focusedWindow itself, as ~Window does.
EventListener<void> goneSub(&window.onMouseEnter, [] { Device::focusedWindow = nullptr; });
Enter(3.0, 4.0);
Check(seen == std::vector<Seen>{ { "enter", 6.0f, 8.0f, true } }, "no move after an enter listener takes the window away");
std::erase(Device::windows, &window);
Device::focusedWindow = nullptr;
if (failures != 0) {
std::println("{} check(s) failed", failures);
return EXIT_FAILURE;
}
std::println("all checks passed");
return EXIT_SUCCESS;
}