Accumulate smooth scroll into full wheel clicks on server

fix(platform/ei): accumulate smooth scroll into full wheel clicks on server

On Wayland, two-finger smooth scroll events are delivered as per-frame pixel
deltas (e.g. 0.26, 0.53, 1.05 px per event).  The previous code quantized
these at the pixel level via `modf()` — any accumulated integer pixel (even
just 1) triggered a wheel event whose value was `pixels * 12`, i.e. 12, 24
or 36 units.  Because the deskflow protocol uses 120 units per standard
wheel click, these fractional clicks were silently dropped by the client
compositor and the user saw nothing on the secondary screen until a
very large/fast stroke pushed enough pixels through.

Switch to wheel-click-level accumulation: smooth scroll pixel deltas are
now scaled to fractional wheel-clicks (0.1 per pixel, keeping the 10 px ==
1 click convention) and only dispatched to the client when at least one
full 120-unit click has accumulated.  This uses `std::trunc()` (toward
zero) instead of `std::floor()` to avoid spurious clicks on negative
scroll values.

Physical mouse wheels (discrete scroll events, EI_EVENT_SCROLL_DISCRETE)
are unaffected and continue to be forwarded immediately as-is.

- `EiScreen.cpp`: replace `modf()` pixel quantization with fractional
  wheel-click accumulation using `std::trunc()`.
- `ScrollRemainder` struct comment: clarify the unit is now fractional
  wheel-clicks rather than pixels.
This commit is contained in:
neon12345 2026-07-16 21:18:42 +02:00 committed by Nick Bolton
parent dd3944cabc
commit 22a4b135f0

View file

@ -28,7 +28,7 @@
#include <unistd.h> #include <unistd.h>
#include <vector> #include <vector>
// Values are in pixels // Values are in fractional wheel-click units (1.0 == one full 120-unit click)
struct ScrollRemainder struct ScrollRemainder
{ {
double x; double x;
@ -762,14 +762,12 @@ void EiScreen::onButtonEvent(ei_event *event)
void EiScreen::onPointerScrollEvent(ei_event *event) void EiScreen::onPointerScrollEvent(ei_event *event)
{ {
// Ratio of 10 pixels == one wheel click because that's what mutter/gtk // Smooth scroll deltas are in pixels. We accumulate them as fractional
// use (for historical reasons). // wheel-click units and only send full wheel clicks (120 units each)
static const int s_pixelsPerWheelClick = 10; // to the client. Sub-120 fractional clicks are silently ignored by
// Our logical wheel clicks are multiples 120, so we // compositors on the receiving end, so accumulating full clicks avoids
// convert between the two and keep the remainders because // flooding the network with events that the client drops anyway.
// we will very likely get subpixel scroll events. static const double s_wheelClicksPerPixel = 0.1; // 10 pixels == 1 full wheel click
// This means a single pixel is 120/s_pixelToWheelRation in wheel values.
const int s_pixelToWheelRatio = s_scrollDelta / s_pixelsPerWheelClick;
assert(m_isPrimary); assert(m_isPrimary);
@ -785,28 +783,31 @@ void EiScreen::onPointerScrollEvent(ei_event *event)
ei_device_set_user_data(device, remainder); ei_device_set_user_data(device, remainder);
} }
dx += remainder->x; // Accumulate smooth scroll as fractional wheel clicks (1.0 == 120 units)
dy += remainder->y; double accX = remainder->x + dx * s_wheelClicksPerPixel;
double accY = remainder->y + dy * s_wheelClicksPerPixel;
double x; // Only dispatch full wheel clicks. Use trunc (toward zero) not floor,
double y; // because floor(-0.3) == -1 which would fire a spurious click.
double rx = modf(dx, &x); double fullClicksX = std::trunc(accX);
double ry = modf(dy, &y); double fullClicksY = std::trunc(accY);
assert(!std::isnan(x) && !std::isinf(x));
assert(!std::isnan(y) && !std::isinf(y));
// libei and deskflow seem to use opposite directions, so we have // libei and deskflow seem to use opposite directions, so we have
// to send the opposite of the value reported by EI if we want to // to send the opposite of the value reported by EI if we want to
// remain compatible with other platforms (including X11). // remain compatible with other platforms (including X11).
if (x != 0 || y != 0) if (fullClicksX != 0 || fullClicksY != 0) {
sendEvent( sendEvent(
EventTypes::PrimaryScreenWheel, EventTypes::PrimaryScreenWheel,
WheelInfo::alloc((int32_t)-x * s_pixelToWheelRatio, (int32_t)-y * s_pixelToWheelRatio) WheelInfo::alloc(
static_cast<int32_t>(-fullClicksX) * s_scrollDelta, static_cast<int32_t>(-fullClicksY) * s_scrollDelta
)
); );
accX -= fullClicksX;
accY -= fullClicksY;
}
remainder->x = rx; remainder->x = accX;
remainder->y = ry; remainder->y = accY;
} }
void EiScreen::onPointerScrollDiscreteEvent(ei_event *event) void EiScreen::onPointerScrollDiscreteEvent(ei_event *event)