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