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:
parent
c608d75a36
commit
419730f46b
8 changed files with 240 additions and 8 deletions
131
tests/MouseScroll/main.cpp
Normal file
131
tests/MouseScroll/main.cpp
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Crafter®.Graphics
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
catcrafts.net
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License version 3.0 as published by the Free Software Foundation;
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
// Regression test for issue #32: the Wayland wl_pointer.axis handler used to
|
||||
// be an empty stub, so Window::onMouseScroll (and with it MouseScrollBind)
|
||||
// never fired on native Linux. This drives Device::PointerListenerHandleAxis
|
||||
// directly — no compositor needed; a default-constructed Window is inert on
|
||||
// Wayland — and asserts the normalization contract documented on
|
||||
// Window::onMouseScroll:
|
||||
//
|
||||
// - one wheel detent (15 axis units, the libinput convention) → ±1,
|
||||
// positive = wheel down, sign packed into the uint32 payload
|
||||
// - sub-detent smooth-scroll values accumulate across events instead of
|
||||
// each truncating to zero
|
||||
// - horizontal axis events are ignored
|
||||
// - no focused window → no event (and no crash)
|
||||
// - the sub-detent remainder is dropped on pointer leave
|
||||
//
|
||||
// The Win32 WM_MOUSEWHEEL path added by the same issue follows the identical
|
||||
// contract but can't be exercised from a Linux test runner.
|
||||
|
||||
#include <wayland-client.h>
|
||||
#include <cstdlib>
|
||||
|
||||
import Crafter.Graphics;
|
||||
import Crafter.Event;
|
||||
import std;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void Check(bool ok, std::string_view what) {
|
||||
std::println("{} {}", ok ? "PASS" : "FAIL", what);
|
||||
if (!ok) ++failures;
|
||||
}
|
||||
|
||||
// Convenience: feed one vertical (or horizontal) axis event, in axis units.
|
||||
void SendAxis(double axisUnits, std::uint32_t axis = WL_POINTER_AXIS_VERTICAL_SCROLL) {
|
||||
Device::PointerListenerHandleAxis(nullptr, nullptr, /*time=*/0, axis,
|
||||
wl_fixed_from_double(axisUnits));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
Window window; // default ctor creates no surface — safe without a compositor
|
||||
std::vector<std::int32_t> deltas;
|
||||
EventListener<std::uint32_t> sub(&window.onMouseScroll,
|
||||
[&](std::uint32_t delta) {
|
||||
// Same reinterpretation consumers (Input::Map) use.
|
||||
deltas.push_back(static_cast<std::int32_t>(delta));
|
||||
});
|
||||
|
||||
Device::focusedWindow = &window;
|
||||
|
||||
// One detent down / one detent up.
|
||||
SendAxis(15.0);
|
||||
Check(deltas == std::vector<std::int32_t>{1}, "one detent down → +1");
|
||||
deltas.clear();
|
||||
SendAxis(-15.0);
|
||||
Check(deltas == std::vector<std::int32_t>{-1}, "one detent up → -1");
|
||||
deltas.clear();
|
||||
|
||||
// Smooth scroll: five 3-unit events must add up to exactly one detent,
|
||||
// delivered on the event that completes it.
|
||||
for (int i = 0; i < 4; ++i) SendAxis(3.0);
|
||||
Check(deltas.empty(), "four 3-unit events → nothing yet");
|
||||
SendAxis(3.0);
|
||||
Check(deltas == std::vector<std::int32_t>{1}, "fifth 3-unit event completes the detent → +1");
|
||||
deltas.clear();
|
||||
|
||||
// Direction reversal mid-accumulation must not misfire.
|
||||
SendAxis(7.5);
|
||||
SendAxis(-7.5);
|
||||
SendAxis(-15.0);
|
||||
Check(deltas == std::vector<std::int32_t>{-1}, "reversal cancels remainder, full detent up → -1");
|
||||
deltas.clear();
|
||||
|
||||
// A big flick delivers multiple detents in one event.
|
||||
SendAxis(45.0);
|
||||
Check(deltas == std::vector<std::int32_t>{3}, "45 axis units in one event → +3");
|
||||
deltas.clear();
|
||||
|
||||
// Horizontal scroll has no Window event — must be ignored entirely
|
||||
// (including the accumulator).
|
||||
SendAxis(15.0, WL_POINTER_AXIS_HORIZONTAL_SCROLL);
|
||||
Check(deltas.empty(), "horizontal axis ignored");
|
||||
SendAxis(14.0);
|
||||
Check(deltas.empty(), "horizontal units did not leak into the vertical accumulator");
|
||||
SendAxis(1.0);
|
||||
Check(deltas == std::vector<std::int32_t>{1}, "vertical accumulator unaffected by horizontal events");
|
||||
deltas.clear();
|
||||
|
||||
// Pointer leave drops the sub-detent remainder.
|
||||
SendAxis(14.0);
|
||||
Device::PointerListenerHandleLeave(nullptr, nullptr, 0, nullptr);
|
||||
Check(Device::focusedWindow == nullptr, "leave clears the focused window");
|
||||
SendAxis(14.0); // no focused window — must be a no-op, not a crash
|
||||
Check(deltas.empty(), "no focused window → no event");
|
||||
Device::focusedWindow = &window;
|
||||
SendAxis(14.0);
|
||||
Check(deltas.empty(), "remainder was reset on leave (14 + 14 stayed < a detent)");
|
||||
SendAxis(1.0);
|
||||
Check(deltas == std::vector<std::int32_t>{1}, "fresh accumulation completes normally");
|
||||
|
||||
Device::focusedWindow = nullptr;
|
||||
if (failures != 0) {
|
||||
std::println("{} check(s) failed", failures);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
std::println("all checks passed");
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue