diff --git a/core/input/mouse.js b/core/input/mouse.js index d479d5c0..95b4e244 100644 --- a/core/input/mouse.js +++ b/core/input/mouse.js @@ -5,8 +5,7 @@ */ import * as Log from '../util/logging.js'; -import { isTouchDevice } from '../util/browser.js'; -import { setCapture, stopEvent, getPointerEvent } from '../util/events.js'; +import { setCapture, stopEvent } from '../util/events.js'; const WHEEL_STEP = 10; // Delta threshold for a mouse wheel step const WHEEL_STEP_TIMEOUT = 50; // ms @@ -17,10 +16,6 @@ export default class Mouse { constructor(target) { this._target = target || document; - this._doubleClickTimer = null; - this._lastTouchPos = null; - - this._pos = null; this._wheelStepXTimer = null; this._wheelStepYTimer = null; this._oldMouseMoveTime = 0; @@ -35,11 +30,6 @@ export default class Mouse { 'mousedisable': this._handleMouseDisable.bind(this) }; - // ===== PROPERTIES ===== - - this.touchButton = 1; // Button mask (1, 2, 4) for touch devices - // (0 means ignore clicks) - // ===== EVENT HANDLERS ===== this.onmousebutton = () => {}; // Handler for mouse button press/release @@ -48,48 +38,11 @@ export default class Mouse { // ===== PRIVATE METHODS ===== - _resetDoubleClickTimer() { - this._doubleClickTimer = null; - } - _handleMouseButton(e, down) { - this._updateMousePosition(e); - let pos = this._pos; + const position = this._getMousePosition(e); let bmask; - if (e.touches || e.changedTouches) { - // Touch device - - // When two touches occur within 500 ms of each other and are - // close enough together a double click is triggered. - if (down == 1) { - if (this._doubleClickTimer === null) { - this._lastTouchPos = pos; - } else { - clearTimeout(this._doubleClickTimer); - - // When the distance between the two touches is small enough - // force the position of the latter touch to the position of - // the first. - - const xs = this._lastTouchPos.x - pos.x; - const ys = this._lastTouchPos.y - pos.y; - const d = Math.sqrt((xs * xs) + (ys * ys)); - - // The goal is to trigger on a certain physical width, - // the devicePixelRatio brings us a bit closer but is - // not optimal. - const threshold = 20 * (window.devicePixelRatio || 1); - if (d < threshold) { - pos = this._lastTouchPos; - } - } - this._doubleClickTimer = - setTimeout(this._resetDoubleClickTimer.bind(this), 500); - } - bmask = this.touchButton; - // If bmask is set - } else if (e.which) { + if (e.which) { /* everything except IE */ bmask = 1 << e.button; } else { @@ -100,18 +53,14 @@ export default class Mouse { } Log.Debug("onmousebutton " + (down ? "down" : "up") + - ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); - this.onmousebutton(pos.x, pos.y, down, bmask); + ", x: " + position.x + ", y: " + position.y + ", bmask: " + bmask); + this.onmousebutton(position.x, position.y, down, bmask); stopEvent(e); } _handleMouseDown(e) { - // Touch events have implicit capture - if (e.type === "mousedown") { - setCapture(this._target); - } - + setCapture(this._target); this._handleMouseButton(e, 1); } @@ -122,27 +71,25 @@ export default class Mouse { // 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() { - + _generateWheelStepX(position) { 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); + this.onmousebutton(position.x, position.y, 1, 1 << 5); + this.onmousebutton(position.x, position.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.onmousebutton(position.x, position.y, 1, 1 << 6); + this.onmousebutton(position.x, position.y, 0, 1 << 6); } this._accumulatedWheelDeltaX = 0; } - _generateWheelStepY() { - + _generateWheelStepY(position) { 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); + this.onmousebutton(position.x, position.y, 1, 1 << 3); + this.onmousebutton(position.x, position.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.onmousebutton(position.x, position.y, 1, 1 << 4); + this.onmousebutton(position.x, position.y, 0, 1 << 4); } this._accumulatedWheelDeltaY = 0; @@ -158,7 +105,7 @@ export default class Mouse { _handleMouseWheel(e) { this._resetWheelStepTimers(); - this._updateMousePosition(e); + const position = this._getMousePosition(e); let dX = e.deltaX; let dY = e.deltaY; @@ -181,17 +128,17 @@ export default class Mouse { // Small delta events that do not pass the threshold get sent // after a timeout. if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) { - this._generateWheelStepX(); + this._generateWheelStepX(position); } else { this._wheelStepXTimer = - window.setTimeout(this._generateWheelStepX.bind(this), + window.setTimeout(() => this._generateWheelStepX(position), WHEEL_STEP_TIMEOUT); } if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) { - this._generateWheelStepY(); + this._generateWheelStepY(position); } else { this._wheelStepYTimer = - window.setTimeout(this._generateWheelStepY.bind(this), + window.setTimeout(() => this._generateWheelStepY(position), WHEEL_STEP_TIMEOUT); } @@ -199,7 +146,7 @@ export default class Mouse { } _handleMouseMove(e) { - this._updateMousePosition(e); + const position = this._getMousePosition(e); // Limit mouse move events to one every MOUSE_MOVE_DELAY ms clearTimeout(this.mouseMoveTimer); @@ -207,9 +154,9 @@ export default class Mouse { if (newMouseMoveTime < this._oldMouseMoveTime + MOUSE_MOVE_DELAY) { this.mouseMoveTimer = setTimeout(this.onmousemove.bind(this), MOUSE_MOVE_DELAY, - this._pos.x, this._pos.y); + position.x, position.y); } else { - this.onmousemove(this._pos.x, this._pos.y); + this.onmousemove(position.x, position.y); } this._oldMouseMoveTime = newMouseMoveTime; @@ -228,9 +175,8 @@ export default class Mouse { } } - // Update coordinates relative to target - _updateMousePosition(e) { - e = getPointerEvent(e); + // Get coordinates relative to target + _getMousePosition(e) { const bounds = this._target.getBoundingClientRect(); let x; let y; @@ -249,18 +195,13 @@ export default class Mouse { } else { y = e.clientY - bounds.top; } - this._pos = {x: x, y: y}; + return { x, y }; } // ===== PUBLIC METHODS ===== grab() { const t = this._target; - if (isTouchDevice) { - t.addEventListener('touchstart', this._eventHandlers.mousedown); - t.addEventListener('touchend', this._eventHandlers.mouseup); - t.addEventListener('touchmove', this._eventHandlers.mousemove); - } t.addEventListener('mousedown', this._eventHandlers.mousedown); t.addEventListener('mouseup', this._eventHandlers.mouseup); t.addEventListener('mousemove', this._eventHandlers.mousemove); @@ -279,11 +220,6 @@ export default class Mouse { this._resetWheelStepTimers(); - if (isTouchDevice) { - t.removeEventListener('touchstart', this._eventHandlers.mousedown); - t.removeEventListener('touchend', this._eventHandlers.mouseup); - t.removeEventListener('touchmove', this._eventHandlers.mousemove); - } t.removeEventListener('mousedown', this._eventHandlers.mousedown); t.removeEventListener('mouseup', this._eventHandlers.mouseup); t.removeEventListener('mousemove', this._eventHandlers.mousemove); diff --git a/core/input/touch.js b/core/input/touch.js new file mode 100644 index 00000000..8dd74a42 --- /dev/null +++ b/core/input/touch.js @@ -0,0 +1,140 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2020 The noVNC Authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import * as Log from '../util/logging.js'; +import { stopEvent, getPointerEvent } from '../util/events.js'; + +const TOUCH_MOVE_DELAY = 17; // Minimum wait (ms) between two touch moves + +export default class Touch { + constructor(target) { + this._target = target || document; + + this._doubleClickTimer = null; + this._lastTouchPos = null; + this._oldTouchMoveTime = 0; + + this._eventHandlers = { + 'touchstart': this._handleTouchStart.bind(this), + 'touchend': this._handleTouchEnd.bind(this), + 'touchmove': this._handleTouchMove.bind(this) + }; + + // ===== PROPERTIES ===== + + this.touchButton = 1; // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) + + // ===== EVENT HANDLERS ===== + + this.ontouch = () => {}; // Handler for mouse button click/release + this.ontouchmove = () => {}; // Handler for mouse movement + } + + // ===== PRIVATE METHODS ===== + + _handleTouchStart(e) { + this._handleTouchAsMouseButton(e, 1); + } + + _handleTouchEnd(e) { + this._handleTouchAsMouseButton(e, 0); + } + + _handleTouchMove(e) { + const position = this._getTouchPosition(e); + + // Limit touch move events to one every TOUCH_MOVE_DELAY ms + clearTimeout(this.touchMoveTimer); + const newTouchMoveTime = Date.now(); + if (newTouchMoveTime < this._oldTouchMoveTime + TOUCH_MOVE_DELAY) { + this.touchMoveTimer = setTimeout(this.ontouchmove.bind(this), + TOUCH_MOVE_DELAY, + position.x, position.y); + } else { + this.ontouchmove(position.x, position.y); + } + this._oldTouchMoveTime = newTouchMoveTime; + + stopEvent(e); + } + + _handleTouchAsMouseButton(e, down) { + let position = this._getTouchPosition(e); + + // When two touches occur within 500 ms of each other and are + // close enough together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = position; + } else { + clearTimeout(this._doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + const xs = this._lastTouchPos.x - position.x; + const ys = this._lastTouchPos.y - position.y; + const d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, + // the devicePixelRatio brings us a bit closer but is + // not optimal. + const threshold = 20 * (window.devicePixelRatio || 1); + if (d < threshold) { + position = this._lastTouchPos; + } + } + this._doubleClickTimer = setTimeout(() => (this._doubleClickTimer = null), 500); + } + + const bmask = this.touchButton; + + Log.Debug("onmousebutton " + (down ? "down" : "up") + + ", x: " + position.x + ", y: " + position.y + ", bmask: " + bmask); + this.ontouch(position.x, position.y, down, bmask); + + stopEvent(e); + } + + // Get coordinates relative to target + _getTouchPosition(e) { + const pointerEvent = getPointerEvent(e); + const bounds = this._target.getBoundingClientRect(); + let x; + let y; + // Clip to target bounds + if (pointerEvent.clientX < bounds.left) { + x = 0; + } else if (pointerEvent.clientX >= bounds.right) { + x = bounds.width - 1; + } else { + x = pointerEvent.clientX - bounds.left; + } + if (pointerEvent.clientY < bounds.top) { + y = 0; + } else if (pointerEvent.clientY >= bounds.bottom) { + y = bounds.height - 1; + } else { + y = pointerEvent.clientY - bounds.top; + } + return { x, y }; + } + + // ===== PUBLIC METHODS ===== + + grab() { + this._target.addEventListener('touchstart', this._eventHandlers.touchstart); + this._target.addEventListener('touchend', this._eventHandlers.touchend); + this._target.addEventListener('touchmove', this._eventHandlers.touchmove); + } + + ungrab() { + this._target.removeEventListener('touchstart', this._eventHandlers.touchstart); + this._target.removeEventListener('touchend', this._eventHandlers.touchend); + this._target.removeEventListener('touchmove', this._eventHandlers.touchmove); + } +} diff --git a/core/rfb.js b/core/rfb.js index 4a8483fd..fc8e4579 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -10,13 +10,14 @@ import { toUnsigned32bit, toSigned32bit } from './util/int.js'; import * as Log from './util/logging.js'; import { encodeUTF8, decodeUTF8 } from './util/strings.js'; -import { dragThreshold } from './util/browser.js'; +import { dragThreshold, isTouchDevice } from './util/browser.js'; import EventTargetMixin from './util/eventtarget.js'; import Display from "./display.js"; import Inflator from "./inflator.js"; import Deflator from "./deflator.js"; import Keyboard from "./input/keyboard.js"; import Mouse from "./input/mouse.js"; +import Touch from "./input/touch.js"; import Cursor from "./util/cursor.js"; import Websock from "./websock.js"; import DES from "./des.js"; @@ -115,6 +116,7 @@ export default class RFB extends EventTargetMixin { this._flushing = false; // Display flushing state this._keyboard = null; // Keyboard input handler object this._mouse = null; // Mouse input handler object + this._touch = null; // Touch input handler object // Timers this._disconnTimer = null; // disconnection timer @@ -202,8 +204,14 @@ export default class RFB extends EventTargetMixin { this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); this._mouse = new Mouse(this._canvas); - this._mouse.onmousebutton = this._handleMouseButton.bind(this); - this._mouse.onmousemove = this._handleMouseMove.bind(this); + this._mouse.onmousebutton = this._handlePointerPress.bind(this); + this._mouse.onmousemove = this._handlePointerMove.bind(this); + + if (isTouchDevice) { + this._touch = new Touch(this._canvas); + this._touch.ontouch = this._handlePointerPress.bind(this); + this._touch.ontouchmove = this._handlePointerMove.bind(this); + } this._sock = new Websock(); this._sock.on('message', () => { @@ -292,17 +300,19 @@ export default class RFB extends EventTargetMixin { if (viewOnly) { this._keyboard.ungrab(); this._mouse.ungrab(); + if (this._touch) { this._touch.ungrab(); } } else { this._keyboard.grab(); this._mouse.grab(); + if (this._touch) { this._touch.grab(); } } } } get capabilities() { return this._capabilities; } - get touchButton() { return this._mouse.touchButton; } - set touchButton(button) { this._mouse.touchButton = button; } + get touchButton() { return this._touch.touchButton; } + set touchButton(button) { this._touch.touchButton = button; } get clipViewport() { return this._clipViewport; } set clipViewport(viewport) { @@ -813,7 +823,7 @@ export default class RFB extends EventTargetMixin { this.sendKey(keysym, code, down); } - _handleMouseButton(x, y, down, bmask) { + _handlePointerPress(x, y, down, bmask) { if (down) { this._mouse_buttonMask |= bmask; } else { @@ -853,7 +863,7 @@ export default class RFB extends EventTargetMixin { RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); } - _handleMouseMove(x, y) { + _handlePointerMove(x, y) { if (this._viewportDragging) { const deltaX = this._viewportDragPos.x - x; const deltaY = this._viewportDragPos.y - y; diff --git a/docs/API-internal.md b/docs/API-internal.md index f1519422..60d5eeb4 100644 --- a/docs/API-internal.md +++ b/docs/API-internal.md @@ -11,8 +11,10 @@ official external API. ## 1.1 Module List -* __Mouse__ (core/input/mouse.js): Mouse input event handler with -limited touch support. +* __Mouse__ (core/input/mouse.js): Mouse input event handler. + +* __Touch__ (core/input/touch.js): Touch input event handler that converts +touch events to mouse events. * __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with non-US keyboard support. Translates keyDown and keyUp events to X11 @@ -39,9 +41,7 @@ callback event name, and the callback function. ### 2.1.1 Configuration Attributes -| name | type | mode | default | description -| ----------- | ---- | ---- | -------- | ------------ -| touchButton | int | RW | 1 | Button mask (1, 2, 4) for which click to send on touch devices. 0 means ignore clicks. +None ### 2.1.2 Methods @@ -58,29 +58,52 @@ callback event name, and the callback function. | onmousemove | (x, y) | Handler for mouse movement -## 2.2 Keyboard Module +## 2.2 Touch Module ### 2.2.1 Configuration Attributes -None +| name | type | mode | default | description +| ----------- | ---- | ---- | -------- | ------------ +| touchButton | int | RW | 1 | Button mask (1, 2, 4) for which click to send on touch devices. 0 means ignore clicks. ### 2.2.2 Methods +| name | parameters | description +| ------ | ---------- | ------------ +| grab | () | Begin capturing touch events +| ungrab | () | Stop capturing touch events + +### 2.2.2 Callbacks + +| name | parameters | description +| ------------- | ------------------- | ------------ +| ontouch | (x, y, down, bmask) | Handler for touch event (as button click/release) +| ontouchmove | (x, y) | Handler for touch movement + + +## 2.3 Keyboard Module + +### 2.3.1 Configuration Attributes + +None + +### 2.3.2 Methods + | name | parameters | description | ------ | ---------- | ------------ | grab | () | Begin capturing keyboard events | ungrab | () | Stop capturing keyboard events -### 2.2.3 Callbacks +### 2.3.4 Callbacks | name | parameters | description | ---------- | -------------------- | ------------ | onkeypress | (keysym, code, down) | Handler for key press/release -## 2.3 Display Module +## 2.4 Display Module -### 2.3.1 Configuration Attributes +### 2.4.1 Configuration Attributes | name | type | mode | default | description | ------------ | ----- | ---- | ------- | ------------ @@ -89,7 +112,7 @@ None | width | int | RO | | Display area width | height | int | RO | | Display area height -### 2.3.2 Methods +### 2.4.2 Methods | name | parameters | description | ------------------ | ------------------------------------------------------- | ------------ @@ -113,7 +136,7 @@ None | drawImage | (img, x, y) | Draw image and track damage | autoscale | (containerWidth, containerHeight) | Scale the display -### 2.3.3 Callbacks +### 2.4.3 Callbacks | name | parameters | description | ------- | ---------- | ------------ diff --git a/tests/test.mouse.js b/tests/test.mouse.js index 5636a713..59f9a6ea 100644 --- a/tests/test.mouse.js +++ b/tests/test.mouse.js @@ -34,7 +34,6 @@ describe('Mouse Event Handling', function () { e.preventDefault = sinon.spy(); return e; }; - const touchevent = mouseevent; describe('Decode Mouse Events', function () { it('should decode mousedown events', function (done) { @@ -89,131 +88,6 @@ describe('Mouse Event Handling', function () { }); }); - describe('Double-click for Touch', function () { - - beforeEach(function () { this.clock = sinon.useFakeTimers(); }); - afterEach(function () { this.clock.restore(); }); - - it('should use same pos for 2nd tap if close enough', function (done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - if (calls === 1) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - } else if (calls === 3) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - done(); - } - }; - // touch events are sent in an array of events - // with one item for each touch point - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); - this.clock.tick(200); - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); - }); - - it('should not modify 2nd tap pos if far apart', function (done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - if (calls === 1) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - } else if (calls === 3) { - expect(down).to.be.equal(1); - expect(x).to.not.be.equal(68); - expect(y).to.not.be.equal(36); - done(); - } - }; - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); - this.clock.tick(200); - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 57, clientY: 35 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 56, clientY: 36 }]})); - }); - - it('should not modify 2nd tap pos if not soon enough', function (done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - if (calls === 1) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - } else if (calls === 3) { - expect(down).to.be.equal(1); - expect(x).to.not.be.equal(68); - expect(y).to.not.be.equal(36); - done(); - } - }; - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); - this.clock.tick(500); - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); - }); - - it('should not modify 2nd tap pos if not touch', function (done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - if (calls === 1) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - } else if (calls === 3) { - expect(down).to.be.equal(1); - expect(x).to.not.be.equal(68); - expect(y).to.not.be.equal(36); - done(); - } - }; - mouse._handleMouseDown(mouseevent( - 'mousedown', { button: '0x01', clientX: 78, clientY: 46 })); - this.clock.tick(10); - mouse._handleMouseUp(mouseevent( - 'mouseup', { button: '0x01', clientX: 79, clientY: 45 })); - this.clock.tick(200); - mouse._handleMouseDown(mouseevent( - 'mousedown', { button: '0x01', clientX: 67, clientY: 35 })); - this.clock.tick(10); - mouse._handleMouseUp(mouseevent( - 'mouseup', { button: '0x01', clientX: 66, clientY: 36 })); - }); - - }); - describe('Accumulate mouse wheel events with small delta', function () { beforeEach(function () { this.clock = sinon.useFakeTimers(); }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index a8975c2e..3b2569e9 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -495,28 +495,28 @@ describe('Remote Frame Buffer Protocol Client', function () { }); it('should not send button messages when initiating viewport dragging', function () { - client._handleMouseButton(13, 9, 0x001); + client._handlePointerPress(13, 9, 0x001); expect(RFB.messages.pointerEvent).to.not.have.been.called; }); it('should send button messages when release without movement', function () { // Just up and down - client._handleMouseButton(13, 9, 0x001); - client._handleMouseButton(13, 9, 0x000); + client._handlePointerPress(13, 9, 0x001); + client._handlePointerPress(13, 9, 0x000); expect(RFB.messages.pointerEvent).to.have.been.calledTwice; RFB.messages.pointerEvent.resetHistory(); // Small movement - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(15, 14); - client._handleMouseButton(15, 14, 0x000); + client._handlePointerPress(13, 9, 0x001); + client._handlePointerMove(15, 14); + client._handlePointerPress(15, 14, 0x000); expect(RFB.messages.pointerEvent).to.have.been.calledTwice; }); it('should send button message directly when drag is disabled', function () { client.dragViewport = false; - client._handleMouseButton(13, 9, 0x001); + client._handlePointerPress(13, 9, 0x001); expect(RFB.messages.pointerEvent).to.have.been.calledOnce; }); @@ -525,15 +525,15 @@ describe('Remote Frame Buffer Protocol Client', function () { // Too small movement - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(18, 9); + client._handlePointerPress(13, 9, 0x001); + client._handlePointerMove(18, 9); expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.not.have.been.called; // Sufficient movement - client._handleMouseMove(43, 9); + client._handlePointerMove(43, 9); expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.have.been.calledOnce; @@ -543,7 +543,7 @@ describe('Remote Frame Buffer Protocol Client', function () { // Now a small movement should move right away - client._handleMouseMove(43, 14); + client._handlePointerMove(43, 14); expect(RFB.messages.pointerEvent).to.not.have.been.called; expect(client._display.viewportChangePos).to.have.been.calledOnce; @@ -553,9 +553,9 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should not send button messages when dragging ends', function () { // First the movement - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(43, 9); - client._handleMouseButton(43, 9, 0x000); + client._handlePointerPress(13, 9, 0x001); + client._handlePointerMove(43, 9); + client._handlePointerPress(43, 9, 0x000); expect(RFB.messages.pointerEvent).to.not.have.been.called; }); @@ -563,15 +563,15 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should terminate viewport dragging on a button up event', function () { // First the dragging movement - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(43, 9); - client._handleMouseButton(43, 9, 0x000); + client._handlePointerPress(13, 9, 0x001); + client._handlePointerMove(43, 9); + client._handlePointerPress(43, 9, 0x000); // Another movement now should not move the viewport sinon.spy(client._display, "viewportChangePos"); - client._handleMouseMove(43, 59); + client._handlePointerMove(43, 59); expect(client._display.viewportChangePos).to.not.have.been.called; }); @@ -2727,26 +2727,26 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should not send button messages in view-only mode', function () { client._viewOnly = true; sinon.spy(client._sock, 'flush'); - client._handleMouseButton(0, 0, 1, 0x001); + client._handlePointerPress(0, 0, 1, 0x001); expect(client._sock.flush).to.not.have.been.called; }); it('should not send movement messages in view-only mode', function () { client._viewOnly = true; sinon.spy(client._sock, 'flush'); - client._handleMouseMove(0, 0); + client._handlePointerMove(0, 0); expect(client._sock.flush).to.not.have.been.called; }); it('should send a pointer event on mouse button presses', function () { - client._handleMouseButton(10, 12, 1, 0x001); + client._handlePointerPress(10, 12, 1, 0x001); const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should send a mask of 1 on mousedown', function () { - client._handleMouseButton(10, 12, 1, 0x001); + client._handlePointerPress(10, 12, 1, 0x001); const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); expect(client._sock).to.have.sent(pointer_msg._sQ); @@ -2754,22 +2754,22 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should send a mask of 0 on mouseup', function () { client._mouse_buttonMask = 0x001; - client._handleMouseButton(10, 12, 0, 0x001); + client._handlePointerPress(10, 12, 0, 0x001); const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should send a pointer event on mouse movement', function () { - client._handleMouseMove(10, 12); + client._handlePointerMove(10, 12); const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); expect(client._sock).to.have.sent(pointer_msg._sQ); }); it('should set the button mask so that future mouse movements use it', function () { - client._handleMouseButton(10, 12, 1, 0x010); - client._handleMouseMove(13, 9); + client._handlePointerPress(10, 12, 1, 0x010); + client._handlePointerMove(13, 9); const pointer_msg = {_sQ: new Uint8Array(12), _sQlen: 0, flush: () => {}}; RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x010); RFB.messages.pointerEvent(pointer_msg, 13, 9, 0x010); diff --git a/tests/test.touch.js b/tests/test.touch.js new file mode 100644 index 00000000..ce4f509e --- /dev/null +++ b/tests/test.touch.js @@ -0,0 +1,135 @@ +const expect = chai.expect; + +import Touch from '../core/input/touch.js'; + +describe('Touch Event Handling', function () { + "use strict"; + + let target; + + beforeEach(function () { + // For these tests we can assume that the canvas is 100x100 + // located at coordinates 10x10 + target = document.createElement('canvas'); + target.style.position = "absolute"; + target.style.top = "10px"; + target.style.left = "10px"; + target.style.width = "100px"; + target.style.height = "100px"; + document.body.appendChild(target); + }); + afterEach(function () { + document.body.removeChild(target); + target = null; + }); + + // The real constructors might not work everywhere we + // want to run these tests + const mouseevent = (typeArg, MouseEventInit) => { + const e = { type: typeArg }; + for (let key in MouseEventInit) { + e[key] = MouseEventInit[key]; + } + e.stopPropagation = sinon.spy(); + e.preventDefault = sinon.spy(); + return e; + }; + const touchevent = mouseevent; + + describe('Double-click for Touch', function () { + + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should use same pos for 2nd tap if close enough', function (done) { + let calls = 0; + const touch = new Touch(target); + touch.ontouch = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + done(); + } + }; + // touch events are sent in an array of events + // with one item for each touch point + touch._handleTouchStart(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + touch._handleTouchEnd(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(200); + touch._handleTouchStart(touchevent( + 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); + this.clock.tick(10); + touch._handleTouchEnd(touchevent( + 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); + }); + + it('should not modify 2nd tap pos if far apart', function (done) { + let calls = 0; + const touch = new Touch(target); + touch.ontouch = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }; + touch._handleTouchStart(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + touch._handleTouchEnd(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(200); + touch._handleTouchStart(touchevent( + 'touchstart', { touches: [{ clientX: 57, clientY: 35 }]})); + this.clock.tick(10); + touch._handleTouchEnd(touchevent( + 'touchend', { touches: [{ clientX: 56, clientY: 36 }]})); + }); + + it('should not modify 2nd tap pos if not soon enough', function (done) { + let calls = 0; + const touch = new Touch(target); + touch.ontouch = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }; + touch._handleTouchStart(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); + this.clock.tick(10); + touch._handleTouchEnd(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); + this.clock.tick(500); + touch._handleTouchStart(touchevent( + 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); + this.clock.tick(10); + touch._handleTouchEnd(touchevent( + 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); + }); + + }); + +});