Merge pull request 'feat(input): wire native mouse-wheel scroll on Wayland + Win32, normalize all backends to ±1/detent' (#34) from claude/issue-32 into master
This commit is contained in:
commit
64a2a1fbd8
8 changed files with 254 additions and 10 deletions
|
|
@ -472,7 +472,22 @@ function domAttachWindow(windowHandle) {
|
||||||
}
|
}
|
||||||
fire("__crafterDom_mouseUp", [e.button]);
|
fire("__crafterDom_mouseUp", [e.button]);
|
||||||
};
|
};
|
||||||
__windowListeners.wheel = (e) => fire("__crafterDom_wheel", [e.deltaY]);
|
// Window::onMouseScroll speaks whole wheel detents (+1 = wheel down),
|
||||||
|
// the same unit the Wayland and Win32 backends deliver. WheelEvent
|
||||||
|
// deltas arrive in browser-specific units, so normalize per deltaMode
|
||||||
|
// (Chromium: ~100px per detent in pixel mode; Firefox: 3 lines in line
|
||||||
|
// mode) and carry the sub-detent remainder so smooth-scroll trackpads
|
||||||
|
// add up instead of each event truncating to zero.
|
||||||
|
let __wheelDetentRemainder = 0;
|
||||||
|
__windowListeners.wheel = (e) => {
|
||||||
|
const perDetent = e.deltaMode === 1 ? 3 : e.deltaMode === 2 ? 1 : 100;
|
||||||
|
__wheelDetentRemainder += e.deltaY / perDetent;
|
||||||
|
const whole = Math.trunc(__wheelDetentRemainder);
|
||||||
|
if (whole !== 0) {
|
||||||
|
__wheelDetentRemainder -= whole;
|
||||||
|
fire("__crafterDom_wheel", [whole]);
|
||||||
|
}
|
||||||
|
};
|
||||||
__windowListeners.contextmenu = (e) => { e.preventDefault(); };
|
__windowListeners.contextmenu = (e) => { e.preventDefault(); };
|
||||||
__windowListeners.pointerlockchange = () => {
|
__windowListeners.pointerlockchange = () => {
|
||||||
// Reset the synthetic accumulator when lock is released so the
|
// Reset the synthetic accumulator when lock is released so the
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,11 @@ void Device::handle_global_remove(void* data, wl_registry* registry, uint32_t na
|
||||||
}
|
}
|
||||||
|
|
||||||
void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_t serial, std::uint32_t time, std::uint32_t button, std::uint32_t state) {
|
void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_t serial, std::uint32_t time, std::uint32_t button, std::uint32_t state) {
|
||||||
|
// Defensive: compositors can deliver pointer events without (or after)
|
||||||
|
// a matching enter — observed on sway during input-device hot-plug.
|
||||||
|
if (focusedWindow == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (button == BTN_LEFT) {
|
if (button == BTN_LEFT) {
|
||||||
if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
|
if(state == WL_POINTER_BUTTON_STATE_PRESSED) {
|
||||||
Device::focusedWindow->mouseLeftHeld = true;
|
Device::focusedWindow->mouseLeftHeld = true;
|
||||||
|
|
@ -243,6 +248,9 @@ void Device::pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_
|
||||||
}
|
}
|
||||||
|
|
||||||
void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) {
|
void Device::PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) {
|
||||||
|
if (focusedWindow == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
|
Vector<float, 2> pos(wl_fixed_to_double(surface_x), wl_fixed_to_double(surface_y));
|
||||||
//Device::focusedWindow->lastMousePos = Device::focusedWindow->currentMousePos;
|
//Device::focusedWindow->lastMousePos = Device::focusedWindow->currentMousePos;
|
||||||
Device::focusedWindow->currentMousePos = pos * Device::focusedWindow->scale;
|
Device::focusedWindow->currentMousePos = pos * Device::focusedWindow->scale;
|
||||||
|
|
@ -267,12 +275,37 @@ void Device::PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std:
|
||||||
}
|
}
|
||||||
|
|
||||||
void Device::PointerListenerHandleLeave(void* data, wl_pointer*, std::uint32_t, wl_surface*) {
|
void Device::PointerListenerHandleLeave(void* data, wl_pointer*, std::uint32_t, wl_surface*) {
|
||||||
Device::focusedWindow->onMouseLeave.Invoke();
|
// Sway has been observed to send a second leave after focus is already
|
||||||
|
// gone (input-device hot-plug) — don't crash on it.
|
||||||
|
if (focusedWindow != nullptr) {
|
||||||
|
focusedWindow->onMouseLeave.Invoke();
|
||||||
focusedWindow = nullptr;
|
focusedWindow = nullptr;
|
||||||
}
|
}
|
||||||
|
// Drop any sub-detent scroll remainder so it can't leak into whatever
|
||||||
|
// window the pointer enters next.
|
||||||
|
scrollDetentRemainder = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
void Device::PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::uint32_t, wl_fixed_t value) {
|
void Device::PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::uint32_t axis, wl_fixed_t value) {
|
||||||
|
// Vertical wheel only — Window has no horizontal-scroll event (yet).
|
||||||
|
if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL || focusedWindow == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// libinput convention (followed by wlroots, Mutter and KWin): one wheel
|
||||||
|
// detent = 15 axis units, positive = wheel down / toward the user — the
|
||||||
|
// same sign as DOM deltaY. Window::onMouseScroll speaks whole detents
|
||||||
|
// (±1 per notch on every backend), so divide and accumulate the
|
||||||
|
// remainder: smooth-scroll devices (touchpads) deliver many sub-detent
|
||||||
|
// events that must add up instead of each truncating to zero.
|
||||||
|
constexpr double axisUnitsPerDetent = 15.0;
|
||||||
|
scrollDetentRemainder += wl_fixed_to_double(value) / axisUnitsPerDetent;
|
||||||
|
const double whole = std::trunc(scrollDetentRemainder);
|
||||||
|
if (whole != 0.0) {
|
||||||
|
scrollDetentRemainder -= whole;
|
||||||
|
// Event payload is uint32 — preserve sign via two's complement.
|
||||||
|
focusedWindow->onMouseScroll.Invoke(
|
||||||
|
static_cast<std::uint32_t>(static_cast<std::int32_t>(whole)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,8 +258,9 @@ void Map::Attach(Window& w) {
|
||||||
scrollListener = std::make_unique<EventListener<std::uint32_t>>(
|
scrollListener = std::make_unique<EventListener<std::uint32_t>>(
|
||||||
&w.onMouseScroll,
|
&w.onMouseScroll,
|
||||||
[this](std::uint32_t delta) {
|
[this](std::uint32_t delta) {
|
||||||
// Wayland and Win32 both deliver scroll as a signed delta
|
// Every backend (DOM, Wayland, Win32) delivers scroll as
|
||||||
// packed into a uint32; we reinterpret to preserve sign.
|
// signed whole detents (+1 = wheel down) packed into a
|
||||||
|
// uint32; we reinterpret to preserve sign.
|
||||||
scrollAccumulator += (std::int32_t)delta;
|
scrollAccumulator += (std::int32_t)delta;
|
||||||
});
|
});
|
||||||
lastMousePos.reset();
|
lastMousePos.reset();
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,25 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case WM_MOUSEWHEEL: {
|
||||||
|
// High word of wParam is the signed wheel movement, WHEEL_DELTA
|
||||||
|
// (120) per detent, positive = wheel away from the user (scroll
|
||||||
|
// up). Window::onMouseScroll speaks whole detents with +1 =
|
||||||
|
// wheel down (the DOM deltaY / Wayland axis sign), so negate.
|
||||||
|
// Free-spinning wheels report sub-WHEEL_DELTA steps — accumulate
|
||||||
|
// so they add up instead of each truncating to zero.
|
||||||
|
static int wheelRemainder = 0;
|
||||||
|
wheelRemainder += -static_cast<int>(GET_WHEEL_DELTA_WPARAM(wParam));
|
||||||
|
int detents = wheelRemainder / WHEEL_DELTA; // trunc toward zero
|
||||||
|
if (detents != 0) {
|
||||||
|
wheelRemainder -= detents * WHEEL_DELTA;
|
||||||
|
// Event payload is uint32 — preserve sign via two's complement.
|
||||||
|
window->onMouseScroll.Invoke(
|
||||||
|
static_cast<std::uint32_t>(static_cast<std::int32_t>(detents)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case WM_SETCURSOR: {
|
case WM_SETCURSOR: {
|
||||||
if (LOWORD(lParam) == HTCLIENT && window->cursorHandle) {
|
if (LOWORD(lParam) == HTCLIENT && window->cursorHandle) {
|
||||||
SetCursor(window->cursorHandle);
|
SetCursor(window->cursorHandle);
|
||||||
|
|
@ -1458,10 +1477,12 @@ extern "C" {
|
||||||
}
|
}
|
||||||
|
|
||||||
__attribute__((export_name("__crafterDom_wheel")))
|
__attribute__((export_name("__crafterDom_wheel")))
|
||||||
void __crafterDom_wheel(std::int32_t /*handle*/, double deltaY) {
|
void __crafterDom_wheel(std::int32_t /*handle*/, double detents) {
|
||||||
if (!g_domWindow) return;
|
if (!g_domWindow) return;
|
||||||
|
// dom-env.js has already normalized WheelEvent deltas to whole
|
||||||
|
// detents (+1 = wheel down) — see __windowListeners.wheel.
|
||||||
// Window::onMouseScroll is uint32 — preserve sign via two's complement.
|
// Window::onMouseScroll is uint32 — preserve sign via two's complement.
|
||||||
g_domWindow->onMouseScroll.Invoke(static_cast<std::uint32_t>(static_cast<std::int32_t>(deltaY)));
|
g_domWindow->onMouseScroll.Invoke(static_cast<std::uint32_t>(static_cast<std::int32_t>(detents)));
|
||||||
}
|
}
|
||||||
|
|
||||||
__attribute__((export_name("__crafterDom_keyDown")))
|
__attribute__((export_name("__crafterDom_keyDown")))
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,12 @@ export namespace Crafter {
|
||||||
// the manager — Clipboard::SetText silently no-ops there.
|
// the manager — Clipboard::SetText silently no-ops there.
|
||||||
inline static wl_data_device_manager* dataDeviceManager = nullptr;
|
inline static wl_data_device_manager* dataDeviceManager = nullptr;
|
||||||
inline static wl_data_device* dataDevice = nullptr;
|
inline static wl_data_device* dataDevice = nullptr;
|
||||||
|
// Sub-detent scroll accumulator for PointerListenerHandleAxis.
|
||||||
|
// wl_pointer.axis values are normalized to wheel detents (15 axis
|
||||||
|
// units each, the libinput convention); whatever doesn't make a
|
||||||
|
// whole detent yet is carried here so smooth-scroll devices add up
|
||||||
|
// across events. Reset on pointer leave.
|
||||||
|
inline static double scrollDetentRemainder = 0.0;
|
||||||
|
|
||||||
static void seat_handle_capabilities(void* data, wl_seat* seat, uint32_t capabilities);
|
static void seat_handle_capabilities(void* data, wl_seat* seat, uint32_t capabilities);
|
||||||
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
|
static void xdg_surface_handle_preferred_scale(void* data, wp_fractional_scale_v1*, std::uint32_t scale);
|
||||||
|
|
@ -92,7 +98,7 @@ export namespace Crafter {
|
||||||
static void keyboard_repeat_info(void *data, wl_keyboard *keyboard, int32_t rate, int32_t delay);
|
static void keyboard_repeat_info(void *data, wl_keyboard *keyboard, int32_t rate, int32_t delay);
|
||||||
static void pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_t serial, std::uint32_t time, std::uint32_t button, std::uint32_t state);
|
static void pointer_handle_button(void* data, wl_pointer* pointer, std::uint32_t serial, std::uint32_t time, std::uint32_t button, std::uint32_t state);
|
||||||
static void PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y);
|
static void PointerListenerHandleMotion(void* data, wl_pointer* wl_pointer, std::uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y);
|
||||||
static void PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t, std::uint32_t, wl_fixed_t value);
|
static void PointerListenerHandleAxis(void*, wl_pointer*, std::uint32_t time, std::uint32_t axis, wl_fixed_t value);
|
||||||
static void PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std::uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y);
|
static void PointerListenerHandleEnter(void* data, wl_pointer* wl_pointer, std::uint32_t serial, wl_surface* surface, wl_fixed_t surface_x, wl_fixed_t surface_y);
|
||||||
static void PointerListenerHandleLeave(void*, wl_pointer*, std::uint32_t, wl_surface*);
|
static void PointerListenerHandleLeave(void*, wl_pointer*, std::uint32_t, wl_surface*);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,15 @@ export namespace Crafter {
|
||||||
Event<void> onMouseMove;
|
Event<void> onMouseMove;
|
||||||
Event<void> onMouseEnter;
|
Event<void> onMouseEnter;
|
||||||
Event<void> onMouseLeave;
|
Event<void> onMouseLeave;
|
||||||
|
// Mouse wheel, in whole detents per event, sign packed into the
|
||||||
|
// uint32 payload (reinterpret as int32 to read it). Positive =
|
||||||
|
// wheel down / toward the user — the DOM deltaY sign. Every
|
||||||
|
// backend normalizes to ±1 per notch: dom-env.js divides
|
||||||
|
// WheelEvent deltas by the per-deltaMode detent size, Wayland
|
||||||
|
// divides the axis value by libinput's 15-units-per-detent,
|
||||||
|
// Win32 divides by WHEEL_DELTA. Sub-detent motion (smooth-scroll
|
||||||
|
// touchpads, free-spinning wheels) is accumulated backend-side
|
||||||
|
// until a whole detent is reached.
|
||||||
Event<std::uint32_t> onMouseScroll;
|
Event<std::uint32_t> onMouseScroll;
|
||||||
Vector<float, 2> currentMousePos;
|
Vector<float, 2> currentMousePos;
|
||||||
Vector<float, 2> lastMousePos;
|
Vector<float, 2> lastMousePos;
|
||||||
|
|
|
||||||
28
project.cpp
28
project.cpp
|
|
@ -237,6 +237,34 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
tc.GetInterfacesAndImplementations(ifaces, testImpls);
|
tc.GetInterfacesAndImplementations(ifaces, testImpls);
|
||||||
pcTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
pcTest.requires_ = { "tool:glslang", "tool:spirv-val" };
|
||||||
cfg.tests.push_back(std::move(pcTest));
|
cfg.tests.push_back(std::move(pcTest));
|
||||||
|
|
||||||
|
// Regression test for issue #32: the Wayland scroll-wheel chain
|
||||||
|
// (wl_pointer.axis → Window::onMouseScroll). Drives the listener
|
||||||
|
// callback directly, so it needs no compositor — but it does need
|
||||||
|
// the Wayland backend compiled in, hence Linux-only.
|
||||||
|
if (!windows) {
|
||||||
|
Test scrollTest;
|
||||||
|
Configuration& sc = scrollTest.config;
|
||||||
|
sc.path = cfg.path;
|
||||||
|
sc.name = "MouseScroll";
|
||||||
|
sc.outputName = "MouseScroll";
|
||||||
|
sc.type = ConfigurationType::Executable;
|
||||||
|
sc.target = cfg.target;
|
||||||
|
sc.march = cfg.march;
|
||||||
|
sc.mtune = cfg.mtune;
|
||||||
|
sc.debug = cfg.debug;
|
||||||
|
sc.sysroot = cfg.sysroot;
|
||||||
|
sc.dependencies = cfg.dependencies;
|
||||||
|
sc.externalDependencies = cfg.externalDependencies;
|
||||||
|
sc.compileFlags = cfg.compileFlags;
|
||||||
|
sc.linkFlags = cfg.linkFlags;
|
||||||
|
sc.defines = cfg.defines;
|
||||||
|
sc.cFiles = cfg.cFiles;
|
||||||
|
std::vector<fs::path> scrollImpls(impls.begin(), impls.end());
|
||||||
|
scrollImpls.emplace_back("tests/MouseScroll/main");
|
||||||
|
sc.GetInterfacesAndImplementations(ifaces, scrollImpls);
|
||||||
|
cfg.tests.push_back(std::move(scrollTest));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
|
|
|
||||||
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