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>
This commit is contained in:
parent
397ff2db51
commit
c62109caf3
7 changed files with 504 additions and 62 deletions
|
|
@ -493,10 +493,7 @@ void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
|
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
|
||||||
//Device::focusedWindow->lastMousePos = Device::focusedWindow->currentMousePos;
|
Device::focusedWindow->PointerMove(pos * Device::focusedWindow->scale);
|
||||||
Device::focusedWindow->currentMousePos = pos * Device::focusedWindow->scale;
|
|
||||||
//Device::focusedWindow->mouseDelta = {Device::focusedWindow->currentMousePos.x-Device::focusedWindow->lastMousePos.x, Device::focusedWindow->currentMousePos.y-Device::focusedWindow->lastMousePos.y};
|
|
||||||
Device::focusedWindow->onMouseMove.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std::uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) {
|
void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std::uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y) {
|
||||||
|
|
@ -509,7 +506,12 @@ void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std:
|
||||||
window->cursorHotspotX_, window->cursorHotspotY_);
|
window->cursorHotspotX_, window->cursorHotspotY_);
|
||||||
}
|
}
|
||||||
focusedWindow = window;
|
focusedWindow = window;
|
||||||
window->onMouseEnter.Invoke();
|
const Vector<float, 2> pos = Vector<float, 2>(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y)) * window->scale;
|
||||||
|
window->PointerEnter(pos);
|
||||||
|
// ~Window clears focusedWindow, should a listener destroy it.
|
||||||
|
if (focusedWindow == window) {
|
||||||
|
window->PointerMove(pos);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -519,12 +521,9 @@ void Device::PointerListenerHandleLeave(void* data, wl_pointer*, std::uint32_t,
|
||||||
// Sway has been observed to send a second leave after focus is already
|
// Sway has been observed to send a second leave after focus is already
|
||||||
// gone (input-device hot-plug) — don't crash on it.
|
// gone (input-device hot-plug) — don't crash on it.
|
||||||
if (focusedWindow != nullptr) {
|
if (focusedWindow != nullptr) {
|
||||||
focusedWindow->onMouseLeave.Invoke();
|
focusedWindow->PointerLeave();
|
||||||
focusedWindow = nullptr;
|
focusedWindow = nullptr;
|
||||||
}
|
}
|
||||||
// Drop any sub-detent scroll remainder so it can't leak into whatever
|
|
||||||
// window the pointer enters next.
|
|
||||||
scrollDetentRemainder = 0.0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Device::PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::uint32_t axis, wl_fixed_t value) {
|
void Device::PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::uint32_t axis, wl_fixed_t value) {
|
||||||
|
|
@ -535,18 +534,11 @@ void Device::PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::u
|
||||||
// libinput convention (followed by wlroots, Mutter and KWin): one wheel
|
// libinput convention (followed by wlroots, Mutter and KWin): one wheel
|
||||||
// detent = 15 axis units, positive = wheel down / toward the user — the
|
// detent = 15 axis units, positive = wheel down / toward the user — the
|
||||||
// same sign as DOM deltaY. Window::onMouseScroll speaks whole detents
|
// same sign as DOM deltaY. Window::onMouseScroll speaks whole detents
|
||||||
// (±1 per notch on every backend), so divide and accumulate the
|
// (±1 per notch on every backend), so divide; smooth-scroll devices
|
||||||
// remainder: smooth-scroll devices (touchpads) deliver many sub-detent
|
// (touchpads) deliver many sub-detent events, which PointerScroll adds
|
||||||
// events that must add up instead of each truncating to zero.
|
// up instead of each truncating to zero.
|
||||||
constexpr double axisUnitsPerDetent = 15.0;
|
constexpr double axisUnitsPerDetent = 15.0;
|
||||||
scrollDetentRemainder += wl_fixed_to_double(value) / axisUnitsPerDetent;
|
focusedWindow->PointerScroll(wl_fixed_to_double(value) / axisUnitsPerDetent);
|
||||||
const double whole = std::trunc(scrollDetentRemainder);
|
|
||||||
if (whole != 0.0) {
|
|
||||||
scrollDetentRemainder -= whole;
|
|
||||||
// Event payload is uint32 — preserve sign via two's complement.
|
|
||||||
focusedWindow->onMouseScroll.Invoke(
|
|
||||||
static_cast<std::uint32_t>(static_cast<std::int32_t>(whole)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ module;
|
||||||
#endif
|
#endif
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <windowsx.h>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#endif
|
#endif
|
||||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
@ -125,31 +126,45 @@ static inline KeyCode KeyCodeFromLParam(LPARAM lParam) {
|
||||||
// Define a window class name
|
// Define a window class name
|
||||||
const char g_szClassName[] = "myWindowClass";
|
const char g_szClassName[] = "myWindowClass";
|
||||||
|
|
||||||
|
// The Window an HWND belongs to, or nullptr once ~Window has detached it.
|
||||||
|
// Looked up again after an Invoke that more work follows: a listener may
|
||||||
|
// have destroyed the Window.
|
||||||
|
static Window* WindowOf(HWND hwnd) {
|
||||||
|
return reinterpret_cast<Window*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
|
||||||
|
}
|
||||||
|
|
||||||
// Window procedure function that processes messages
|
// Window procedure function that processes messages
|
||||||
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||||
|
|
||||||
Window* window = nullptr;
|
|
||||||
|
|
||||||
if (msg == WM_NCCREATE)
|
if (msg == WM_NCCREATE)
|
||||||
{
|
{
|
||||||
CREATESTRUCT* pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
|
CREATESTRUCT* pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
|
||||||
window = static_cast<Window*>(pCreate->lpCreateParams);
|
Window* window = static_cast<Window*>(pCreate->lpCreateParams);
|
||||||
|
|
||||||
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(window));
|
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(window));
|
||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
// Messages before WM_NCCREATE, and those DestroyWindow sends from
|
||||||
window = reinterpret_cast<Window*>(
|
// ~Window, have no Window to go to.
|
||||||
GetWindowLongPtr(hwnd, GWLP_USERDATA)
|
Window* window = WindowOf(hwnd);
|
||||||
);
|
if (window == nullptr) {
|
||||||
|
return DefWindowProc(hwnd, msg, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (msg) {
|
switch (msg) {
|
||||||
case WM_DESTROY:{
|
case WM_CLOSE: {
|
||||||
PostQuitMessage(0);
|
// A request, as xdg_toplevel.close is: StartSync's loop ends
|
||||||
break;
|
// and the HWND lives on until ~Window destroys it, so the
|
||||||
|
// swapchain never presents to a window that is gone.
|
||||||
|
// DefWindowProc would destroy it here.
|
||||||
|
window->onClose.Invoke();
|
||||||
|
window = WindowOf(hwnd);
|
||||||
|
if (window != nullptr) {
|
||||||
|
window->open = false;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
case WM_SIZE: {
|
case WM_SIZE: {
|
||||||
// SIZE_MINIMIZED reports (0, 0) — Resize() short-circuits, so
|
// SIZE_MINIMIZED reports (0, 0) — Resize() short-circuits, so
|
||||||
|
|
@ -220,46 +235,95 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case WM_MOUSEMOVE: {
|
||||||
|
// Signed: under capture, a drag that has left the window keeps
|
||||||
|
// reporting positions past the client area.
|
||||||
|
const POINT point{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
|
||||||
|
const Vector<float, 2> position(static_cast<float>(point.x), static_cast<float>(point.y));
|
||||||
|
// Win32 has no enter message: the first move over the client
|
||||||
|
// area after a leave is the enter. TME_LEAVE is one-shot, so it
|
||||||
|
// is armed again for each enter, and the enter only counts once
|
||||||
|
// it is armed, or its leave would never come.
|
||||||
|
RECT client;
|
||||||
|
if (!window->mouseInside && GetClientRect(hwnd, &client) && PtInRect(&client, point)) {
|
||||||
|
TRACKMOUSEEVENT track{sizeof(track), TME_LEAVE, hwnd, 0};
|
||||||
|
if (TrackMouseEvent(&track)) {
|
||||||
|
window->PointerEnter(position);
|
||||||
|
window = WindowOf(hwnd);
|
||||||
|
if (window == nullptr) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window->PointerMove(position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WM_MOUSELEAVE: {
|
||||||
|
// Also sent when the pointer moves onto the non-client area
|
||||||
|
// (title bar, borders) — it has left the drawable surface.
|
||||||
|
window->PointerLeave();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// A press captures the mouse, so a drag that leaves the window
|
||||||
|
// still sends its moves and its release here, as Wayland's
|
||||||
|
// implicit grab does. The capture goes when no button is held.
|
||||||
case WM_LBUTTONDOWN: {
|
case WM_LBUTTONDOWN: {
|
||||||
|
SetCapture(hwnd);
|
||||||
window->mouseLeftHeld = true;
|
window->mouseLeftHeld = true;
|
||||||
window->onMouseLeftClick.Invoke();
|
window->onMouseLeftClick.Invoke();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case WM_LBUTTONUP: {
|
case WM_LBUTTONUP: {
|
||||||
|
// Not held: the press went to another window, or losing the
|
||||||
|
// capture has released it already.
|
||||||
|
if (!window->mouseLeftHeld) break;
|
||||||
window->mouseLeftHeld = false;
|
window->mouseLeftHeld = false;
|
||||||
|
if (!window->mouseRightHeld && GetCapture() == hwnd) ReleaseCapture();
|
||||||
window->onMouseLeftRelease.Invoke();
|
window->onMouseLeftRelease.Invoke();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case WM_RBUTTONDOWN: {
|
case WM_RBUTTONDOWN: {
|
||||||
|
SetCapture(hwnd);
|
||||||
window->mouseRightHeld = true;
|
window->mouseRightHeld = true;
|
||||||
window->onMouseRightClick.Invoke();
|
window->onMouseRightClick.Invoke();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
case WM_RBUTTONUP: {
|
case WM_RBUTTONUP: {
|
||||||
|
if (!window->mouseRightHeld) break;
|
||||||
window->mouseRightHeld = false;
|
window->mouseRightHeld = false;
|
||||||
|
if (!window->mouseLeftHeld && GetCapture() == hwnd) ReleaseCapture();
|
||||||
window->onMouseRightRelease.Invoke();
|
window->onMouseRightRelease.Invoke();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case WM_CAPTURECHANGED: {
|
||||||
|
// The capture was taken mid-drag (another window took it, the
|
||||||
|
// window lost activation), so the release will not come here:
|
||||||
|
// release what is still held. The ReleaseCapture above lands
|
||||||
|
// here with nothing held.
|
||||||
|
if (reinterpret_cast<HWND>(lParam) == hwnd) break;
|
||||||
|
if (window->mouseLeftHeld) {
|
||||||
|
window->mouseLeftHeld = false;
|
||||||
|
window->onMouseLeftRelease.Invoke();
|
||||||
|
window = WindowOf(hwnd);
|
||||||
|
if (window == nullptr) break;
|
||||||
|
}
|
||||||
|
if (window->mouseRightHeld) {
|
||||||
|
window->mouseRightHeld = false;
|
||||||
|
window->onMouseRightRelease.Invoke();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case WM_MOUSEWHEEL: {
|
case WM_MOUSEWHEEL: {
|
||||||
// High word of wParam is the signed wheel movement, WHEEL_DELTA
|
// High word of wParam is the signed wheel movement, WHEEL_DELTA
|
||||||
// (120) per detent, positive = wheel away from the user (scroll
|
// (120) per detent, positive = wheel away from the user (scroll
|
||||||
// up). Window::onMouseScroll speaks whole detents with +1 =
|
// up). Window::onMouseScroll speaks detents with +1 = wheel down
|
||||||
// wheel down (the DOM deltaY / Wayland axis sign), so negate.
|
// (the DOM deltaY / Wayland axis sign), so negate. Free-spinning
|
||||||
// Free-spinning wheels report sub-WHEEL_DELTA steps — accumulate
|
// wheels report sub-WHEEL_DELTA steps, which PointerScroll adds
|
||||||
// so they add up instead of each truncating to zero.
|
// up instead of each truncating to zero.
|
||||||
static int wheelRemainder = 0;
|
window->PointerScroll(-GET_WHEEL_DELTA_WPARAM(wParam) / static_cast<double>(WHEEL_DELTA));
|
||||||
wheelRemainder += -static_cast<int>(GET_WHEEL_DELTA_WPARAM(wParam));
|
|
||||||
int detents = wheelRemainder / WHEEL_DELTA; // trunc toward zero
|
|
||||||
if (detents != 0) {
|
|
||||||
wheelRemainder -= detents * WHEEL_DELTA;
|
|
||||||
// Event payload is uint32 — preserve sign via two's complement.
|
|
||||||
window->onMouseScroll.Invoke(
|
|
||||||
static_cast<std::uint32_t>(static_cast<std::int32_t>(detents)));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,6 +348,11 @@ Window::Window(std::uint32_t width, std::uint32_t height, const std::string_view
|
||||||
}
|
}
|
||||||
|
|
||||||
Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height(height) {
|
Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height(height) {
|
||||||
|
// Before the platform code below, which may report the pointer.
|
||||||
|
lastMousePos = {0,0};
|
||||||
|
mouseDelta = {0,0};
|
||||||
|
currentMousePos = {0,0};
|
||||||
|
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||||
Device::windows.push_back(this);
|
Device::windows.push_back(this);
|
||||||
surface = wl_compositor_create_surface(Device::compositor);
|
surface = wl_compositor_create_surface(Device::compositor);
|
||||||
|
|
@ -318,14 +387,15 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
||||||
wc.lpszClassName = g_szClassName;
|
wc.lpszClassName = g_szClassName;
|
||||||
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||||
|
|
||||||
if (!RegisterClass(&wc)) {
|
// The first window registers the class; every later one finds it there.
|
||||||
|
if (!RegisterClass(&wc) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
|
||||||
MessageBox(NULL, "Window Class Registration Failed!", "Error", MB_ICONERROR);
|
MessageBox(NULL, "Window Class Registration Failed!", "Error", MB_ICONERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
RECT rc = {0, 0, static_cast<LONG>(width), static_cast<LONG>(height)};
|
RECT rc = {0, 0, static_cast<LONG>(width), static_cast<LONG>(height)};
|
||||||
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
|
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
|
||||||
|
|
||||||
HWND hwnd = CreateWindowEx(
|
hwnd = CreateWindowEx(
|
||||||
0,
|
0,
|
||||||
g_szClassName,
|
g_szClassName,
|
||||||
"",
|
"",
|
||||||
|
|
@ -345,8 +415,12 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
||||||
ShowWindow(hwnd, SW_SHOWNORMAL);
|
ShowWindow(hwnd, SW_SHOWNORMAL);
|
||||||
UpdateWindow(hwnd);
|
UpdateWindow(hwnd);
|
||||||
|
|
||||||
|
// Input stays queued for StartSync. Dispatched here, it would reach the
|
||||||
|
// window before anyone could listen: the WM_MOUSEMOVE Windows sends
|
||||||
|
// when a window appears under the cursor would make an onMouseEnter
|
||||||
|
// nobody hears, and later an onMouseLeave without one.
|
||||||
MSG msg;
|
MSG msg;
|
||||||
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
|
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_QS_POSTMESSAGE | PM_QS_PAINT | PM_QS_SENDMESSAGE)) {
|
||||||
TranslateMessage(&msg);
|
TranslateMessage(&msg);
|
||||||
DispatchMessage(&msg);
|
DispatchMessage(&msg);
|
||||||
}
|
}
|
||||||
|
|
@ -511,10 +585,6 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
||||||
presentInfo.pSwapchains = &swapChain;
|
presentInfo.pSwapchains = &swapChain;
|
||||||
presentInfo.pImageIndices = ¤tBuffer;
|
presentInfo.pImageIndices = ¤tBuffer;
|
||||||
presentInfo.waitSemaphoreCount = 1;
|
presentInfo.waitSemaphoreCount = 1;
|
||||||
|
|
||||||
lastMousePos = {0,0};
|
|
||||||
mouseDelta = {0,0};
|
|
||||||
currentMousePos = {0,0};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
|
@ -566,9 +636,50 @@ Window::~Window() {
|
||||||
}
|
}
|
||||||
if (Device::textInputWindow == this) Device::textInputWindow = nullptr;
|
if (Device::textInputWindow == this) Device::textInputWindow = nullptr;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||||
|
// Detached first: what DestroyWindow sends (WM_CAPTURECHANGED,
|
||||||
|
// WM_DESTROY) and anything still queued for the HWND must not reach
|
||||||
|
// this Window.
|
||||||
|
if (hwnd != nullptr) {
|
||||||
|
SetWindowLongPtr(hwnd, GWLP_USERDATA, 0);
|
||||||
|
DestroyWindow(hwnd);
|
||||||
|
hwnd = nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void Window::PointerEnter(Vector<float, 2> position) {
|
||||||
|
currentMousePos = position;
|
||||||
|
if (mouseInside) return;
|
||||||
|
mouseInside = true;
|
||||||
|
onMouseEnter.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::PointerMove(Vector<float, 2> position) {
|
||||||
|
currentMousePos = position;
|
||||||
|
onMouseMove.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::PointerLeave() {
|
||||||
|
// A partial detent from before must not count towards the next scroll,
|
||||||
|
// after a leave without an enter too.
|
||||||
|
scrollRemainder = 0.0;
|
||||||
|
if (!mouseInside) return;
|
||||||
|
mouseInside = false;
|
||||||
|
onMouseLeave.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::PointerScroll(double detents) {
|
||||||
|
scrollRemainder += detents;
|
||||||
|
const double whole = std::trunc(scrollRemainder);
|
||||||
|
if (whole == 0.0) return;
|
||||||
|
scrollRemainder -= whole;
|
||||||
|
// Event payload is uint32 — preserve sign via two's complement.
|
||||||
|
onMouseScroll.Invoke(static_cast<std::uint32_t>(static_cast<std::int32_t>(whole)));
|
||||||
|
}
|
||||||
|
|
||||||
void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
|
void Window::Resize(std::uint32_t newWidth, std::uint32_t newHeight) {
|
||||||
// Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
|
// Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
|
||||||
// sometimes echoes the current size in a configure.
|
// sometimes echoes the current size in a configure.
|
||||||
|
|
|
||||||
|
|
@ -83,12 +83,6 @@ export namespace Crafter {
|
||||||
// the manager — Clipboard::SetText silently no-ops there.
|
// the manager — Clipboard::SetText silently no-ops there.
|
||||||
inline static wl_data_device_manager* dataDeviceManager = nullptr;
|
inline static wl_data_device_manager* dataDeviceManager = nullptr;
|
||||||
inline static wl_data_device* dataDevice = nullptr;
|
inline static wl_data_device* dataDevice = nullptr;
|
||||||
// Sub-detent scroll accumulator for PointerListenerHandleAxis.
|
|
||||||
// wl_pointer.axis values are normalized to wheel detents (15 axis
|
|
||||||
// units each, the libinput convention); whatever doesn't make a
|
|
||||||
// whole detent yet is carried here so smooth-scroll devices add up
|
|
||||||
// across events. Reset on pointer leave.
|
|
||||||
inline static double scrollDetentRemainder = 0.0;
|
|
||||||
// zwp_text_input_v3: one object per seat; the compositor says which
|
// zwp_text_input_v3: one object per seat; the compositor says which
|
||||||
// surface it follows (enter/leave), the events are state that done()
|
// surface it follows (enter/leave), the events are state that done()
|
||||||
// applies as a whole.
|
// applies as a whole.
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,9 @@ export namespace Crafter {
|
||||||
// WheelEvent deltas by the per-deltaMode detent size, Wayland
|
// WheelEvent deltas by the per-deltaMode detent size, Wayland
|
||||||
// divides the axis value by libinput's 15-units-per-detent,
|
// divides the axis value by libinput's 15-units-per-detent,
|
||||||
// Win32 divides by WHEEL_DELTA. Sub-detent motion (smooth-scroll
|
// Win32 divides by WHEEL_DELTA. Sub-detent motion (smooth-scroll
|
||||||
// touchpads, free-spinning wheels) is accumulated backend-side
|
// touchpads, free-spinning wheels) is accumulated until a whole
|
||||||
// until a whole detent is reached.
|
// detent is reached — by PointerScroll natively, by dom-env.js in
|
||||||
|
// the browser — and dropped when the pointer leaves.
|
||||||
Event<std::uint32_t> onMouseScroll;
|
Event<std::uint32_t> onMouseScroll;
|
||||||
// Touch, from the seat's wl_touch. The first finger down also drives
|
// Touch, from the seat's wl_touch. The first finger down also drives
|
||||||
// currentMousePos and the left-button events until it lifts, so a
|
// currentMousePos and the left-button events until it lifts, so a
|
||||||
|
|
@ -144,6 +145,28 @@ export namespace Crafter {
|
||||||
bool mouseLeftHeld = false;
|
bool mouseLeftHeld = false;
|
||||||
bool mouseRightHeld = false;
|
bool mouseRightHeld = false;
|
||||||
|
|
||||||
|
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||||
|
// The pointer is over the window: set before onMouseEnter fires,
|
||||||
|
// cleared before onMouseLeave does.
|
||||||
|
bool mouseInside = false;
|
||||||
|
// The part of a wheel detent PointerScroll has not reported yet.
|
||||||
|
double scrollRemainder = 0.0;
|
||||||
|
// The backends report the pointer through these, so that enter,
|
||||||
|
// move, leave and scroll mean the same on each. PointerEnter puts
|
||||||
|
// the entry position in currentMousePos before onMouseEnter fires,
|
||||||
|
// and an enter is a move too: the backend follows it with
|
||||||
|
// PointerMove. PointerLeave drops the scroll remainder, and fires
|
||||||
|
// only after an enter. Each fires its event last, since a listener
|
||||||
|
// may destroy the window, so a backend that makes a second call
|
||||||
|
// first checks that the window still exists.
|
||||||
|
void PointerEnter(Vector<float, 2> position);
|
||||||
|
void PointerMove(Vector<float, 2> position);
|
||||||
|
void PointerLeave();
|
||||||
|
// Adds wheel motion in detents, fractions included, and fires
|
||||||
|
// onMouseScroll for each whole detent it adds up to.
|
||||||
|
void PointerScroll(double detents);
|
||||||
|
#endif
|
||||||
|
|
||||||
Window() = default;
|
Window() = default;
|
||||||
Window(std::uint32_t width, std::uint32_t height);
|
Window(std::uint32_t width, std::uint32_t height);
|
||||||
Window(std::uint32_t width, std::uint32_t height, const std::string_view title);
|
Window(std::uint32_t width, std::uint32_t height, const std::string_view title);
|
||||||
|
|
@ -160,8 +183,10 @@ export namespace Crafter {
|
||||||
// the swapchain and the Vulkan surface, then (Wayland) the cursor,
|
// the swapchain and the Vulkan surface, then (Wayland) the cursor,
|
||||||
// viewport, fractional scale, decoration, xdg toplevel, xdg surface
|
// viewport, fractional scale, decoration, xdg toplevel, xdg surface
|
||||||
// and wl_surface, and the window leaves Device::windows and the
|
// and wl_surface, and the window leaves Device::windows and the
|
||||||
// focus pointers — so a window can be closed and another created,
|
// focus pointers, or (Win32) the HWND, detached from this Window
|
||||||
// which a resident app does on every hide and show.
|
// first so that no message still meant for it arrives here — so a
|
||||||
|
// window can be closed and another created, which a resident app
|
||||||
|
// does on every hide and show.
|
||||||
~Window();
|
~Window();
|
||||||
|
|
||||||
// Enter the platform event loop and never return. In DOM mode this
|
// Enter the platform event loop and never return. In DOM mode this
|
||||||
|
|
@ -232,6 +257,7 @@ export namespace Crafter {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||||
|
HWND hwnd = nullptr;
|
||||||
HBITMAP cursorBitmap = nullptr;
|
HBITMAP cursorBitmap = nullptr;
|
||||||
HCURSOR cursorHandle = nullptr;
|
HCURSOR cursorHandle = nullptr;
|
||||||
std::uint16_t cursorSizeX = 0;
|
std::uint16_t cursorSizeX = 0;
|
||||||
|
|
|
||||||
17
project.cpp
17
project.cpp
|
|
@ -326,6 +326,18 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
pcTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
pcTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
||||||
cfg.tests.push_back(std::move(pcTest));
|
cfg.tests.push_back(std::move(pcTest));
|
||||||
|
|
||||||
|
// The Win32 backend's pointer, capture, close and teardown handling,
|
||||||
|
// through a real HWND: posts it the messages Windows would and checks
|
||||||
|
// what the Window made of them. The Window needs Vulkan for its
|
||||||
|
// swapchain. Built on Linux it runs under Wine, which may be absent.
|
||||||
|
if (windows) {
|
||||||
|
Test win32PointerTest = LibraryTest("PointerWin32");
|
||||||
|
#ifndef _WIN32
|
||||||
|
win32PointerTest.requires_ = { "tool:wine" };
|
||||||
|
#endif
|
||||||
|
cfg.tests.push_back(std::move(win32PointerTest));
|
||||||
|
}
|
||||||
|
|
||||||
// Regression test for issue #32: the Wayland scroll-wheel chain
|
// Regression test for issue #32: the Wayland scroll-wheel chain
|
||||||
// (wl_pointer.axis → Window::onMouseScroll). Drives the listener
|
// (wl_pointer.axis → Window::onMouseScroll). Drives the listener
|
||||||
// callback directly, so it needs no compositor — but it does need
|
// callback directly, so it needs no compositor — but it does need
|
||||||
|
|
@ -333,6 +345,11 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
if (!windows) {
|
if (!windows) {
|
||||||
cfg.tests.push_back(LibraryTest("MouseScroll"));
|
cfg.tests.push_back(LibraryTest("MouseScroll"));
|
||||||
|
|
||||||
|
// The pointer's enter, move and leave through the helpers both
|
||||||
|
// native backends report with (Window::PointerEnter and co.).
|
||||||
|
// Drives the wl_pointer callbacks directly, as MouseScroll does.
|
||||||
|
cfg.tests.push_back(LibraryTest("PointerEnterLeave"));
|
||||||
|
|
||||||
// Issue #40: multi-frame-in-flight frame pacing (per-image fences
|
// Issue #40: multi-frame-in-flight frame pacing (per-image fences
|
||||||
// + per-frame semaphores, no steady-state wait-idle). Drives the
|
// + per-frame semaphores, no steady-state wait-idle). Drives the
|
||||||
// real frame loop against a live Wayland compositor for many more
|
// real frame loop against a live Wayland compositor for many more
|
||||||
|
|
|
||||||
122
tests/PointerEnterLeave/main.cpp
Normal file
122
tests/PointerEnterLeave/main.cpp
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
//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;
|
||||||
|
}
|
||||||
180
tests/PointerWin32/main.cpp
Normal file
180
tests/PointerWin32/main.cpp
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
//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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue