Crafter.Graphics/tests/MouseScroll/main.cpp

131 lines
5 KiB
C++
Raw Permalink Normal View History

/*
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;
}