feat(input): wire native mouse-wheel scroll on Wayland + Win32, normalize all backends to ±1/detent (#32)

Window::onMouseScroll now fires on every backend and speaks one unit:
signed whole detents packed in the uint32 payload, +1 = wheel down
(DOM deltaY sign).

- Wayland: implement the previously-stubbed PointerListenerHandleAxis —
  vertical axis only, wl_fixed_to_double / 15 (libinput's units-per-
  detent), sub-detent remainder accumulated and reset on pointer leave.
- Win32: add the missing WM_MOUSEWHEEL case — -GET_WHEEL_DELTA_WPARAM /
  WHEEL_DELTA with a remainder accumulator for free-spinning wheels.
- DOM: dom-env.js normalizes WheelEvent.deltaY per deltaMode (100px /
  3 lines / 1 page per detent) with the same remainder scheme, so wasm
  delivers the identical unit instead of raw browser-specific deltas.
- Document the contract on Window::onMouseScroll.
- tests/MouseScroll: compositor-free regression test driving the
  Wayland axis handler directly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-12 15:02:28 +00:00
commit 419730f46b
8 changed files with 240 additions and 8 deletions

View file

@ -257,6 +257,25 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
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)));
}
break;
}
case WM_SETCURSOR: {
if (LOWORD(lParam) == HTCLIENT && window->cursorHandle) {
SetCursor(window->cursorHandle);
@ -1458,10 +1477,12 @@ extern "C" {
}
__attribute__((export_name("__crafterDom_wheel")))
void __crafterDom_wheel(std::int32_t /*handle*/, double deltaY) {
void __crafterDom_wheel(std::int32_t /*handle*/, double detents) {
if (!g_domWindow) return;
// dom-env.js has already normalized WheelEvent deltas to whole
// detents (+1 = wheel down) — see __windowListeners.wheel.
// Window::onMouseScroll is uint32 — preserve sign via two's complement.
g_domWindow->onMouseScroll.Invoke(static_cast<std::uint32_t>(static_cast<std::int32_t>(deltaY)));
g_domWindow->onMouseScroll.Invoke(static_cast<std::uint32_t>(static_cast<std::int32_t>(detents)));
}
__attribute__((export_name("__crafterDom_keyDown")))