//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // 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 #include 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 deltas; EventListener sub(&window.onMouseScroll, [&](std::uint32_t delta) { // Same reinterpretation consumers (Input::Map) use. deltas.push_back(static_cast(delta)); }); Device::focusedWindow = &window; // One detent down / one detent up. SendAxis(15.0); Check(deltas == std::vector{1}, "one detent down → +1"); deltas.clear(); SendAxis(-15.0); Check(deltas == std::vector{-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{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{-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{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{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{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; }