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;
|
||||
}
|
||||
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
|
||||
//Device::focusedWindow->lastMousePos = Device::focusedWindow->currentMousePos;
|
||||
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();
|
||||
Device::focusedWindow->PointerMove(pos * Device::focusedWindow->scale);
|
||||
}
|
||||
|
||||
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_);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
// gone (input-device hot-plug) — don't crash on it.
|
||||
if (focusedWindow != nullptr) {
|
||||
focusedWindow->onMouseLeave.Invoke();
|
||||
focusedWindow->PointerLeave();
|
||||
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) {
|
||||
|
|
@ -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
|
||||
// detent = 15 axis units, positive = wheel down / toward the user — the
|
||||
// same sign as DOM deltaY. Window::onMouseScroll speaks whole detents
|
||||
// (±1 per notch on every backend), so divide and accumulate the
|
||||
// remainder: smooth-scroll devices (touchpads) deliver many sub-detent
|
||||
// events that must add up instead of each truncating to zero.
|
||||
// (±1 per notch on every backend), so divide; smooth-scroll devices
|
||||
// (touchpads) deliver many sub-detent events, which PointerScroll adds
|
||||
// up instead of each truncating to zero.
|
||||
constexpr double axisUnitsPerDetent = 15.0;
|
||||
scrollDetentRemainder += 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)));
|
||||
}
|
||||
focusedWindow->PointerScroll(wl_fixed_to_double(value) / axisUnitsPerDetent);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ module;
|
|||
#endif
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
|
||||
#include <windows.h>
|
||||
#include <windowsx.h>
|
||||
#include <cassert>
|
||||
#endif
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
|
|
@ -125,31 +126,45 @@ static inline KeyCode KeyCodeFromLParam(LPARAM lParam) {
|
|||
// Define a window class name
|
||||
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
|
||||
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
|
||||
Window* window = nullptr;
|
||||
|
||||
if (msg == WM_NCCREATE)
|
||||
{
|
||||
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));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
else
|
||||
{
|
||||
window = reinterpret_cast<Window*>(
|
||||
GetWindowLongPtr(hwnd, GWLP_USERDATA)
|
||||
);
|
||||
|
||||
// Messages before WM_NCCREATE, and those DestroyWindow sends from
|
||||
// ~Window, have no Window to go to.
|
||||
Window* window = WindowOf(hwnd);
|
||||
if (window == nullptr) {
|
||||
return DefWindowProc(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
switch (msg) {
|
||||
case WM_DESTROY:{
|
||||
PostQuitMessage(0);
|
||||
break;
|
||||
case WM_CLOSE: {
|
||||
// A request, as xdg_toplevel.close is: StartSync's loop ends
|
||||
// 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: {
|
||||
// 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;
|
||||
}
|
||||
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: {
|
||||
SetCapture(hwnd);
|
||||
window->mouseLeftHeld = true;
|
||||
window->onMouseLeftClick.Invoke();
|
||||
break;
|
||||
}
|
||||
|
||||
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->onMouseLeftRelease.Invoke();
|
||||
if (!window->mouseRightHeld && GetCapture() == hwnd) ReleaseCapture();
|
||||
window->onMouseLeftRelease.Invoke();
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_RBUTTONDOWN: {
|
||||
SetCapture(hwnd);
|
||||
window->mouseRightHeld = true;
|
||||
window->onMouseRightClick.Invoke();
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_RBUTTONUP: {
|
||||
if (!window->mouseRightHeld) break;
|
||||
window->mouseRightHeld = false;
|
||||
if (!window->mouseLeftHeld && GetCapture() == hwnd) ReleaseCapture();
|
||||
window->onMouseRightRelease.Invoke();
|
||||
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: {
|
||||
// High word of wParam is the signed wheel movement, WHEEL_DELTA
|
||||
// (120) per detent, positive = wheel away from the user (scroll
|
||||
// up). Window::onMouseScroll speaks whole detents with +1 =
|
||||
// wheel down (the DOM deltaY / Wayland axis sign), so negate.
|
||||
// Free-spinning wheels report sub-WHEEL_DELTA steps — accumulate
|
||||
// so they add up instead of each truncating to zero.
|
||||
static int wheelRemainder = 0;
|
||||
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)));
|
||||
}
|
||||
// up). Window::onMouseScroll speaks detents with +1 = wheel down
|
||||
// (the DOM deltaY / Wayland axis sign), so negate. Free-spinning
|
||||
// wheels report sub-WHEEL_DELTA steps, which PointerScroll adds
|
||||
// up instead of each truncating to zero.
|
||||
window->PointerScroll(-GET_WHEEL_DELTA_WPARAM(wParam) / static_cast<double>(WHEEL_DELTA));
|
||||
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) {
|
||||
// Before the platform code below, which may report the pointer.
|
||||
lastMousePos = {0,0};
|
||||
mouseDelta = {0,0};
|
||||
currentMousePos = {0,0};
|
||||
|
||||
#ifdef CRAFTER_GRAPHICS_WINDOW_WAYLAND
|
||||
Device::windows.push_back(this);
|
||||
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.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);
|
||||
}
|
||||
|
||||
RECT rc = {0, 0, static_cast<LONG>(width), static_cast<LONG>(height)};
|
||||
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
|
||||
HWND hwnd = CreateWindowEx(
|
||||
hwnd = CreateWindowEx(
|
||||
0,
|
||||
g_szClassName,
|
||||
"",
|
||||
|
|
@ -345,8 +415,12 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
|||
ShowWindow(hwnd, SW_SHOWNORMAL);
|
||||
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;
|
||||
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);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
|
@ -511,10 +585,6 @@ Window::Window(std::uint32_t width, std::uint32_t height) : width(width), height
|
|||
presentInfo.pSwapchains = &swapChain;
|
||||
presentInfo.pImageIndices = ¤tBuffer;
|
||||
presentInfo.waitSemaphoreCount = 1;
|
||||
|
||||
lastMousePos = {0,0};
|
||||
mouseDelta = {0,0};
|
||||
currentMousePos = {0,0};
|
||||
}
|
||||
|
||||
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
|
||||
|
|
@ -566,9 +636,50 @@ Window::~Window() {
|
|||
}
|
||||
if (Device::textInputWindow == this) Device::textInputWindow = nullptr;
|
||||
#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
|
||||
|
||||
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) {
|
||||
// Skip degenerate resizes. Win32 minimised windows give (0, 0); Wayland
|
||||
// sometimes echoes the current size in a configure.
|
||||
|
|
|
|||
Loading…
Reference in a new issue