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>
122 lines
4.5 KiB
C++
122 lines
4.5 KiB
C++
//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;
|
|
}
|