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>
131 lines
5 KiB
C++
131 lines
5 KiB
C++
/*
|
|
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;
|
|
}
|