From 28b004fd70818e4410cbb3ad62ef27206948172d Mon Sep 17 00:00:00 2001 From: Samuel Mannehed Date: Tue, 12 Sep 2017 11:16:24 +0200 Subject: [PATCH] Combine small mouse wheel events The VNC protocol can't handle different deltas or speeds for a mouse wheel event. When using a device that sends a lot of small mouse wheel events, instead of fewer larger steps, the effect was that mouse wheel scrolling was way to sensitive. This patch looks at the delta of wheel events and doesn't send events until the combined delta has passed a threshold. Single events that doesn't pass the threshold get sent after a timeout in order to not loose any events. Fixes #577. --- core/input/mouse.js | 114 +++++++++++++++++++++++++++++++++++--------- tests/test.mouse.js | 87 +++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 23 deletions(-) diff --git a/core/input/mouse.js b/core/input/mouse.js index f17f9b93..2e758074 100644 --- a/core/input/mouse.js +++ b/core/input/mouse.js @@ -13,11 +13,21 @@ import { isTouchDevice } from '../util/browsers.js'; import { setCapture, stopEvent, getPointerEvent } from '../util/events.js'; import { set_defaults, make_properties } from '../util/properties.js'; +var WHEEL_STEP = 10; // Delta threshold for a mouse wheel step +var WHEEL_STEP_TIMEOUT = 50; // ms +var WHEEL_LINE_HEIGHT = 19; + export default function Mouse(defaults) { this._doubleClickTimer = null; this._lastTouchPos = null; + this._pos = null; + this._wheelStepXTimer = null; + this._wheelStepYTimer = null; + this._accumulatedWheelDeltaX = 0; + this._accumulatedWheelDeltaY = 0; + // Configuration attributes set_defaults(this, defaults, { 'target': document, @@ -44,7 +54,8 @@ Mouse.prototype = { _handleMouseButton: function (e, down) { if (!this._focused) { return; } - var pos = this._getMousePosition(e); + this._updateMousePosition(e); + var pos = this._pos; var bmask; if (e.touches || e.changedTouches) { @@ -108,27 +119,82 @@ Mouse.prototype = { this._handleMouseButton(e, 0); }, + // Mouse wheel events are sent in steps over VNC. This means that the VNC + // protocol can't handle a wheel event with specific distance or speed. + // Therefor, if we get a lot of small mouse wheel events we combine them. + _generateWheelStepX: function () { + + if (this._accumulatedWheelDeltaX < 0) { + this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 5); + this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 5); + } else if (this._accumulatedWheelDeltaX > 0) { + this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 6); + this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 6); + } + + this._accumulatedWheelDeltaX = 0; + }, + + _generateWheelStepY: function () { + + if (this._accumulatedWheelDeltaY < 0) { + this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 3); + this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 3); + } else if (this._accumulatedWheelDeltaY > 0) { + this._onMouseButton(this._pos.x, this._pos.y, 1, 1 << 4); + this._onMouseButton(this._pos.x, this._pos.y, 0, 1 << 4); + } + + this._accumulatedWheelDeltaY = 0; + }, + + _resetWheelStepTimers: function () { + window.clearTimeout(this._wheelStepXTimer); + window.clearTimeout(this._wheelStepYTimer); + this._wheelStepXTimer = null; + this._wheelStepYTimer = null; + }, + _handleMouseWheel: function (e) { - if (!this._focused) { return; } + if (!this._focused || !this._onMouseButton) { return; } - var pos = this._getMousePosition(e); + this._resetWheelStepTimers(); - if (this._onMouseButton) { - if (e.deltaX < 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 5); - this._onMouseButton(pos.x, pos.y, 0, 1 << 5); - } else if (e.deltaX > 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 6); - this._onMouseButton(pos.x, pos.y, 0, 1 << 6); - } + this._updateMousePosition(e); - if (e.deltaY < 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 3); - this._onMouseButton(pos.x, pos.y, 0, 1 << 3); - } else if (e.deltaY > 0) { - this._onMouseButton(pos.x, pos.y, 1, 1 << 4); - this._onMouseButton(pos.x, pos.y, 0, 1 << 4); - } + var dX = e.deltaX; + var dY = e.deltaY; + + // Pixel units unless it's non-zero. + // Note that if deltamode is line or page won't matter since we aren't + // sending the mouse wheel delta to the server anyway. + // The difference between pixel and line can be important however since + // we have a threshold that can be smaller than the line height. + if (e.deltaMode !== 0) { + dX *= WHEEL_LINE_HEIGHT; + dY *= WHEEL_LINE_HEIGHT; + } + + this._accumulatedWheelDeltaX += dX; + this._accumulatedWheelDeltaY += dY; + + // Generate a mouse wheel step event when the accumulated delta + // for one of the axes is large enough. + // Small delta events that do not pass the threshold get sent + // after a timeout. + if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) { + this._generateWheelStepX(); + } else { + this._wheelStepXTimer = + window.setTimeout(this._generateWheelStepX.bind(this), + WHEEL_STEP_TIMEOUT); + } + if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) { + this._generateWheelStepY(); + } else { + this._wheelStepYTimer = + window.setTimeout(this._generateWheelStepY.bind(this), + WHEEL_STEP_TIMEOUT); } stopEvent(e); @@ -137,9 +203,9 @@ Mouse.prototype = { _handleMouseMove: function (e) { if (! this._focused) { return; } - var pos = this._getMousePosition(e); + this._updateMousePosition(e); if (this._onMouseMove) { - this._onMouseMove(pos.x, pos.y); + this._onMouseMove(this._pos.x, this._pos.y); } stopEvent(e); }, @@ -158,8 +224,8 @@ Mouse.prototype = { } }, - // Return coordinates relative to target - _getMousePosition: function(e) { + // Update coordinates relative to target + _updateMousePosition: function(e) { e = getPointerEvent(e); var bounds = this._target.getBoundingClientRect(); var x, y; @@ -178,7 +244,7 @@ Mouse.prototype = { } else { y = e.clientY - bounds.top; } - return {x:x, y:y}; + this._pos = {x:x, y:y}; }, // Public methods @@ -206,6 +272,8 @@ Mouse.prototype = { ungrab: function () { var c = this._target; + this._resetWheelStepTimers(); + if (isTouchDevice) { c.removeEventListener('touchstart', this._eventHandlers.mousedown); c.removeEventListener('touchend', this._eventHandlers.mouseup); diff --git a/tests/test.mouse.js b/tests/test.mouse.js index d75340e4..e6ff754b 100644 --- a/tests/test.mouse.js +++ b/tests/test.mouse.js @@ -223,4 +223,91 @@ describe('Mouse Event Handling', function() { }); + describe('Accumulate mouse wheel events with small delta', function() { + + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should accumulate wheel events if small enough', function () { + var callback = sinon.spy(); + var mouse = new Mouse({ onMouseButton: callback, target: target }); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 4, deltaY: 0, deltaMode: 0 })); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 4, deltaY: 0, deltaMode: 0 })); + + // threshold is 10 + expect(mouse._accumulatedWheelDeltaX).to.be.equal(8); + + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 4, deltaY: 0, deltaMode: 0 })); + + expect(callback).to.have.callCount(2); // mouse down and up + + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 4, deltaY: 9, deltaMode: 0 })); + + expect(mouse._accumulatedWheelDeltaX).to.be.equal(4); + expect(mouse._accumulatedWheelDeltaY).to.be.equal(9); + + expect(callback).to.have.callCount(2); // still + }); + + it('should not accumulate large wheel events', function () { + var callback = sinon.spy(); + var mouse = new Mouse({ onMouseButton: callback, target: target }); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 11, deltaY: 0, deltaMode: 0 })); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 0, deltaY: 70, deltaMode: 0 })); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 400, deltaY: 400, deltaMode: 0 })); + + expect(callback).to.have.callCount(8); // mouse down and up + }); + + it('should send even small wheel events after a timeout', function () { + var callback = sinon.spy(); + var mouse = new Mouse({ onMouseButton: callback, target: target }); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 1, deltaY: 0, deltaMode: 0 })); + this.clock.tick(51); // timeout on 50 ms + + expect(callback).to.have.callCount(2); // mouse down and up + }); + + it('should account for non-zero deltaMode', function () { + var callback = sinon.spy(); + var mouse = new Mouse({ onMouseButton: callback, target: target }); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 0, deltaY: 2, deltaMode: 1 })); + + this.clock.tick(10); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { clientX: 18, clientY: 40, + deltaX: 1, deltaY: 0, deltaMode: 2 })); + + expect(callback).to.have.callCount(4); // mouse down and up + }); + }); + });