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

@ -269,10 +269,31 @@ void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std:
void Device::PointerListenerHandleLeave(void* data, wl_pointer*, std::uint32_t, wl_surface*) {
Device::focusedWindow->onMouseLeave.Invoke();
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, wl_fixed_t value) {
void Device::PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::uint32_t axis, wl_fixed_t value) {
// Vertical wheel only — Window has no horizontal-scroll event (yet).
if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL || focusedWindow == nullptr) {
return;
}
// 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.
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)));
}
}