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:
Jorijn van der Graaf 2026-10-31 03:29:50 +01:00
commit c62109caf3
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
7 changed files with 504 additions and 62 deletions

View file

@ -83,12 +83,6 @@ export namespace Crafter {
// the manager — Clipboard::SetText silently no-ops there.
inline static wl_data_device_manager* dataDeviceManager = 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
// surface it follows (enter/leave), the events are state that done()
// applies as a whole.

View file

@ -122,8 +122,9 @@ export namespace Crafter {
// WheelEvent deltas by the per-deltaMode detent size, Wayland
// divides the axis value by libinput's 15-units-per-detent,
// Win32 divides by WHEEL_DELTA. Sub-detent motion (smooth-scroll
// touchpads, free-spinning wheels) is accumulated backend-side
// until a whole detent is reached.
// touchpads, free-spinning wheels) is accumulated until a whole
// detent is reached — by PointerScroll natively, by dom-env.js in
// the browser — and dropped when the pointer leaves.
Event<std::uint32_t> onMouseScroll;
// Touch, from the seat's wl_touch. The first finger down also drives
// currentMousePos and the left-button events until it lifts, so a
@ -144,6 +145,28 @@ export namespace Crafter {
bool mouseLeftHeld = 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(std::uint32_t width, std::uint32_t height);
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,
// viewport, fractional scale, decoration, xdg toplevel, xdg surface
// and wl_surface, and the window leaves Device::windows and the
// focus pointers — so a window can be closed and another created,
// which a resident app does on every hide and show.
// focus pointers, or (Win32) the HWND, detached from this Window
// 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();
// Enter the platform event loop and never return. In DOM mode this
@ -232,6 +257,7 @@ export namespace Crafter {
#endif
#ifdef CRAFTER_GRAPHICS_WINDOW_WIN32
HWND hwnd = nullptr;
HBITMAP cursorBitmap = nullptr;
HCURSOR cursorHandle = nullptr;
std::uint16_t cursorSizeX = 0;