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

Merged
catbot merged 2 commits from claude/issue-32 into master 2026-06-13 02:10:53 +02:00
Member

Resolves #32

Window::onMouseScroll was only wired on the DOM/wasm backend; both native window backends silently dropped wheel input, so MouseScrollBind actions were dead on native (found while wiring 3DForts#164).

What changed

All three backends now deliver one normalized unit, documented on Window::onMouseScroll: signed whole detents packed in the uint32 payload, +1 = wheel down (DOM deltaY sign). Sub-detent motion (smooth-scroll touchpads, free-spin wheels) accumulates backend-side until a whole detent is reached, so it adds up instead of truncating to zero per event.

  • WaylandDevice::PointerListenerHandleAxis implemented (was an empty stub): vertical axis only, wl_fixed_to_double(value) / 15 (libinput's axis-units-per-detent, followed by wlroots/Mutter/KWin), remainder carried in Device::scrollDetentRemainder and reset on pointer leave.
  • Win32 — added the missing WM_MOUSEWHEEL case: -GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA (negated because Win32 positive = wheel away from user) with the same remainder scheme.
  • DOMdom-env.js now normalizes WheelEvent.deltaY per deltaMode (100 px / 3 lines / 1 page per detent) instead of forwarding raw browser-specific deltas. ⚠️ Behavioral change on wasm: a Chromium detent used to arrive as ±100, a Firefox detent as ±3; both are now ±1, identical to native. If 3DForts tuned scroll sensitivity against raw deltaY, that factor moves into app-side scale — in exchange the binding behaves the same on every platform.
  • Bonus fix — the Wayland pointer handlers (leave/motion/button) dereferenced Device::focusedWindow unconditionally; sway delivers a second leave after focus is already cleared during input-device hot-plug, which segfaulted. Now guarded.

Verification

  • tests/MouseScroll (new, runs in crafter-build test): drives the Wayland axis handler directly — detent down/up, 5×3-unit smooth-scroll accumulation, direction reversal mid-accumulation, 3-detent flick, horizontal-axis isolation, null-focus safety, remainder reset on leave. 13 checks, all green.
  • Wayland end-to-end: ran the InputSystem example under the headless sway, injected real wheel events via a zwlr_virtual_pointer_v1 client (15 → [Zoom] delta +1, −15 → −1, 45 → 3 detents, 7.5+7.5 → one detent on completion). Confirms compositor → wl_pointer.axisonMouseScrollMouseScrollBind → Axis1D action, with sway's real 15-units-per-detent values.
  • DOM end-to-end: HelloDom in Firefox, synthetic WheelEvents: {100,px}→+1, {-100,px}→−1, {3,line}→+1, {300,px}→+3, {50,px}×2→+1.
  • Win32: can't link the full Windows target here (pre-existing mingw WinRT gap in Gamepad.cpp stops the cross-build before Window.cpp), so the new case was syntax-checked against mingw's winuser.h and its semantics unit-checked standalone (one detent forward → uint32(-1)).
  • crafter-build test: 2 passed.
Resolves #32 `Window::onMouseScroll` was only wired on the DOM/wasm backend; both native window backends silently dropped wheel input, so `MouseScrollBind` actions were dead on native (found while wiring 3DForts#164). ## What changed All three backends now deliver one normalized unit, documented on `Window::onMouseScroll`: **signed whole detents** packed in the uint32 payload, **+1 = wheel down** (DOM deltaY sign). Sub-detent motion (smooth-scroll touchpads, free-spin wheels) accumulates backend-side until a whole detent is reached, so it adds up instead of truncating to zero per event. - **Wayland** — `Device::PointerListenerHandleAxis` implemented (was an empty stub): vertical axis only, `wl_fixed_to_double(value) / 15` (libinput's axis-units-per-detent, followed by wlroots/Mutter/KWin), remainder carried in `Device::scrollDetentRemainder` and reset on pointer leave. - **Win32** — added the missing `WM_MOUSEWHEEL` case: `-GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA` (negated because Win32 positive = wheel away from user) with the same remainder scheme. - **DOM** — `dom-env.js` now normalizes `WheelEvent.deltaY` per `deltaMode` (100 px / 3 lines / 1 page per detent) instead of forwarding raw browser-specific deltas. ⚠️ **Behavioral change on wasm**: a Chromium detent used to arrive as ±100, a Firefox detent as ±3; both are now ±1, identical to native. If 3DForts tuned scroll sensitivity against raw deltaY, that factor moves into app-side scale — in exchange the binding behaves the same on every platform. - **Bonus fix** — the Wayland pointer handlers (leave/motion/button) dereferenced `Device::focusedWindow` unconditionally; sway delivers a second `leave` after focus is already cleared during input-device hot-plug, which segfaulted. Now guarded. ## Verification - `tests/MouseScroll` (new, runs in `crafter-build test`): drives the Wayland axis handler directly — detent down/up, 5×3-unit smooth-scroll accumulation, direction reversal mid-accumulation, 3-detent flick, horizontal-axis isolation, null-focus safety, remainder reset on leave. 13 checks, all green. - **Wayland end-to-end**: ran the InputSystem example under the headless sway, injected real wheel events via a `zwlr_virtual_pointer_v1` client (15 → `[Zoom] delta +1`, −15 → `−1`, 45 → 3 detents, 7.5+7.5 → one detent on completion). Confirms compositor → `wl_pointer.axis` → `onMouseScroll` → `MouseScrollBind` → Axis1D action, with sway's real 15-units-per-detent values. - **DOM end-to-end**: HelloDom in Firefox, synthetic WheelEvents: `{100,px}→+1`, `{-100,px}→−1`, `{3,line}→+1`, `{300,px}→+3`, `{50,px}×2→+1`. - **Win32**: can't link the full Windows target here (pre-existing mingw WinRT gap in Gamepad.cpp stops the cross-build before Window.cpp), so the new case was syntax-checked against mingw's `winuser.h` and its semantics unit-checked standalone (one detent forward → `uint32(-1)`). - `crafter-build test`: 2 passed.
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>
Sway delivers pointer events in surprising orders around input-device
hot-plug — observed: a second wl_pointer.leave after focus was already
cleared, which crashed in PointerListenerHandleLeave (null deref), and
motion/button would crash the same way if an enter never matched one of
our surfaces. Early-return instead. Found while end-to-end-testing the
scroll chain for #32 with a zwlr_virtual_pointer_v1 injector.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
catbot merged commit 64a2a1fbd8 into master 2026-06-13 02:10:53 +02:00
catbot deleted branch claude/issue-32 2026-06-13 02:10:53 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Catcrafts/Crafter.Graphics!34
No description provided.