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 + }); + }); + });