Implement TouchpadMode gesture handling.

This commit is contained in:
Jared Goodwin 2024-02-04 11:17:43 -08:00
parent 810d294b18
commit 6175985967
6 changed files with 457 additions and 41 deletions

View File

@ -88,7 +88,7 @@ const UI = {
}); });
// Adapt the interface for touch screen devices // Adapt the interface for touch screen devices
if (isTouchDevice) { if (isTouchDevice()) {
// Remove the address bar // Remove the address bar
setTimeout(() => window.scrollTo(0, 1), 100); setTimeout(() => window.scrollTo(0, 1), 100);
} }
@ -464,6 +464,12 @@ const UI = {
.classList.remove('noVNC_open'); .classList.remove('noVNC_open');
}, },
/**
* @param {string} text
* @param { "normal" | "info" | "warn" | "warning" | "error" } statusType
* @param {number} time
* @returns
*/
showStatus(text, statusType, time) { showStatus(text, statusType, time) {
const statusElem = document.getElementById('noVNC_status'); const statusElem = document.getElementById('noVNC_status');
@ -1064,8 +1070,10 @@ const UI = {
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); UI.rfb.compressionLevel = parseInt(UI.getSetting('compression'));
UI.rfb.showDotCursor = UI.getSetting('show_dot'); UI.rfb.showDotCursor = UI.getSetting('show_dot');
UI.rfb.touchpadMode = WebUtil.readSetting('touchpad_mode', 'false') === 'true';
UI.updateViewOnly(); // requires UI.rfb UI.updateViewOnly(); // requires UI.rfb
UI.updateTouchpadMode();
}, },
disconnect() { disconnect() {
@ -1119,6 +1127,12 @@ const UI = {
// Do this last because it can only be used on rendered elements // Do this last because it can only be used on rendered elements
UI.rfb.focus(); UI.rfb.focus();
// In touchpad mode, we want the cursor centered in the
// viewport at the start so we can see it.
if (UI.rfb.touchpadMode) {
UI.rfb.centerCursorInViewport();
}
}, },
disconnectFinished(e) { disconnectFinished(e) {
@ -1348,7 +1362,7 @@ const UI = {
// Can't be clipping if viewport is scaled to fit // Can't be clipping if viewport is scaled to fit
UI.forceSetting('view_clip', false); UI.forceSetting('view_clip', false);
UI.rfb.clipViewport = false; UI.rfb.clipViewport = false;
} else if (brokenScrollbars) { } else if (brokenScrollbars || UI.rfb.touchpadMode) {
UI.forceSetting('view_clip', true); UI.forceSetting('view_clip', true);
UI.rfb.clipViewport = true; UI.rfb.clipViewport = true;
} else { } else {
@ -1372,6 +1386,7 @@ const UI = {
UI.rfb.dragViewport = !UI.rfb.dragViewport; UI.rfb.dragViewport = !UI.rfb.dragViewport;
UI.updateViewDrag(); UI.updateViewDrag();
UI.updateTouchpadMode();
}, },
updateViewDrag() { updateViewDrag() {
@ -1379,6 +1394,10 @@ const UI = {
const viewDragButton = document.getElementById('noVNC_view_drag_button'); const viewDragButton = document.getElementById('noVNC_view_drag_button');
if (UI.rfb.dragViewport) {
UI.rfb.touchpadMode = false;
}
if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) && if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) &&
UI.rfb.dragViewport) { UI.rfb.dragViewport) {
// We are no longer clipping the viewport. Make sure // We are no longer clipping the viewport. Make sure
@ -1432,7 +1451,7 @@ const UI = {
* ------v------*/ * ------v------*/
showVirtualKeyboard() { showVirtualKeyboard() {
if (!isTouchDevice) return; if (!isTouchDevice()) return;
const input = document.getElementById('noVNC_keyboardinput'); const input = document.getElementById('noVNC_keyboardinput');
@ -1450,7 +1469,7 @@ const UI = {
}, },
hideVirtualKeyboard() { hideVirtualKeyboard() {
if (!isTouchDevice) return; if (!isTouchDevice()) return;
const input = document.getElementById('noVNC_keyboardinput'); const input = document.getElementById('noVNC_keyboardinput');
@ -1599,12 +1618,33 @@ const UI = {
if (!UI.rfb) return; if (!UI.rfb) return;
UI.rfb.touchpadMode = !UI.rfb.touchpadMode; UI.rfb.touchpadMode = !UI.rfb.touchpadMode;
UI.updateTouchpadButton(); WebUtil.writeSetting('touchpad_mode', UI.rfb.touchpadMode);
UI.updateTouchpadMode();
UI.updateViewDrag();
}, },
updateTouchpadButton() { updateTouchpadMode() {
const touchpadButton = document.getElementById('noVNC_touchpad_button'); if (UI.rfb.touchpadMode) {
UI.rfb.dragViewport = false;
UI.forceSetting('resize', 'off');
UI.forceSetting('view_clip', true);
UI.forceSetting('show_dot', true);
UI.rfb.clipViewport = true;
UI.rfb.scaleViewport = false;
UI.rfb.resizeSession = false;
UI.rfb.showDotCursor = true;
}
else {
UI.enableSetting('resize');
UI.enableSetting('view_clip');
UI.enableSetting('show_dot');
}
UI.updateViewDrag
const touchpadButton = document.getElementById('noVNC_touchpad_button');
if (UI.rfb.touchpadMode) { if (UI.rfb.touchpadMode) {
touchpadButton.classList.add("noVNC_selected"); touchpadButton.classList.add("noVNC_selected");
} else { } else {
@ -1730,12 +1770,14 @@ const UI = {
.classList.add('noVNC_hidden'); .classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button') document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden'); .classList.add('noVNC_hidden');
document.getElementById('noVNC_clipboard_button')
.classList.add('noVNC_hidden');
} else { } else {
document.getElementById('noVNC_keyboard_button') document.getElementById('noVNC_keyboard_button')
.classList.remove('noVNC_hidden'); .classList.remove('noVNC_hidden');
document.getElementById('noVNC_toggle_extra_keys_button') document.getElementById('noVNC_toggle_extra_keys_button')
.classList.remove('noVNC_hidden'); .classList.remove('noVNC_hidden');
document.getElementById('noVNC_clipboard_button') document.getElementById('noVNC_touchpad_button')
.classList.remove('noVNC_hidden'); .classList.remove('noVNC_hidden');
} }
}, },

View File

@ -87,8 +87,38 @@ export default class Display {
return this._fbHeight; return this._fbHeight;
} }
get viewportLocation() {
return this._viewportLoc;
}
// ===== PUBLIC METHODS ===== // ===== PUBLIC METHODS =====
/**
* Attempt to move the viewport by the specified amounts
* and returns the amount of actual position change.
* @param {number} moveByX
* @param {number} moveByY
* @return {{ x: number, y: number }}
*/
viewportTryMoveBy(moveByX, moveByY) {
if (moveByX === 0 && moveByY === 0) {
return {
x: 0,
y: 0
}
}
const vpX = this._viewportLoc.x;
const vpY = this._viewportLoc.y;
this.viewportChangePos(moveByX, moveByY);
return {
x: this._viewportLoc.x - vpX,
y: this._viewportLoc.y - vpY
}
}
viewportChangePos(deltaX, deltaY) { viewportChangePos(deltaX, deltaY) {
const vp = this._viewportLoc; const vp = this._viewportLoc;
deltaX = Math.floor(deltaX); deltaX = Math.floor(deltaX);
@ -433,6 +463,10 @@ export default class Display {
this._rescale(scaleRatio); this._rescale(scaleRatio);
} }
rescale(factor) {
this._rescale(factor);
}
// ===== PRIVATE METHODS ===== // ===== PRIVATE METHODS =====
_rescale(factor) { _rescale(factor) {

View File

@ -18,7 +18,6 @@ const GH_PINCH = 64;
const GH_INITSTATE = 127; const GH_INITSTATE = 127;
const GH_MOVE_THRESHOLD = 50;
const GH_ANGLE_THRESHOLD = 90; // Degrees const GH_ANGLE_THRESHOLD = 90; // Degrees
// Timeout when waiting for gestures (ms) // Timeout when waiting for gestures (ms)
@ -38,6 +37,7 @@ export default class GestureHandler {
this._target = null; this._target = null;
this._state = GH_INITSTATE; this._state = GH_INITSTATE;
this._touchpadMode = false;
this._tracked = []; this._tracked = [];
this._ignored = []; this._ignored = [];
@ -51,6 +51,37 @@ export default class GestureHandler {
this._boundEventHandler = this._eventHandler.bind(this); this._boundEventHandler = this._eventHandler.bind(this);
} }
// ===== PROPERTIES =====
/**
* @returns {boolean}
*/
get touchpadMode() {
return this._touchpadMode;
}
/**
* @param {boolean} enabled
*/
set touchpadMode(enabled) {
this._touchpadMode = enabled;
}
/**
* @returns {number}
*/
get _ghMoveThreshold() {
// In TouchpadMode, we want movements to be very precise,
// so we'll reduce the movement threshold.
if (this._touchpadMode) {
return 5;
}
return 50;
}
// ===== PUBLIC METHODS =====
attach(target) { attach(target) {
this.detach(); this.detach();
@ -64,7 +95,6 @@ export default class GestureHandler {
this._target.addEventListener('touchcancel', this._target.addEventListener('touchcancel',
this._boundEventHandler); this._boundEventHandler);
} }
detach() { detach() {
if (!this._target) { if (!this._target) {
return; return;
@ -84,6 +114,10 @@ export default class GestureHandler {
this._target = null; this._target = null;
} }
/**
*
* @param {TouchEvent} e
*/
_eventHandler(e) { _eventHandler(e) {
let fn; let fn;
@ -102,7 +136,6 @@ export default class GestureHandler {
fn = this._touchEnd; fn = this._touchEnd;
break; break;
} }
for (let i = 0; i < e.changedTouches.length; i++) { for (let i = 0; i < e.changedTouches.length; i++) {
let touch = e.changedTouches[i]; let touch = e.changedTouches[i];
fn.call(this, touch.identifier, touch.clientX, touch.clientY); fn.call(this, touch.identifier, touch.clientX, touch.clientY);
@ -142,7 +175,9 @@ export default class GestureHandler {
firstY: y, firstY: y,
lastX: x, lastX: x,
lastY: y, lastY: y,
angle: 0 movementX: 0,
movementY: 0,
angle: 0,
}); });
switch (this._tracked.length) { switch (this._tracked.length) {
@ -173,6 +208,8 @@ export default class GestureHandler {
} }
// Update the touches last position with the event coordinates // Update the touches last position with the event coordinates
touch.movementX = x - touch.lastX;
touch.movementY = y - touch.lastY;
touch.lastX = x; touch.lastX = x;
touch.lastY = y; touch.lastY = y;
@ -187,7 +224,7 @@ export default class GestureHandler {
if (!this._hasDetectedGesture()) { if (!this._hasDetectedGesture()) {
// Ignore moves smaller than the minimum threshold // Ignore moves smaller than the minimum threshold
if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) { if (Math.hypot(deltaX, deltaY) < this._ghMoveThreshold) {
return; return;
} }
@ -216,7 +253,7 @@ export default class GestureHandler {
// We know that the current touch moved far enough, // We know that the current touch moved far enough,
// but unless both touches moved further than their // but unless both touches moved further than their
// threshold we don't want to disqualify any gestures // threshold we don't want to disqualify any gestures
if (prevDeltaMove > GH_MOVE_THRESHOLD) { if (prevDeltaMove > this._ghMoveThreshold) {
// The angle difference between the direction of the touch points // The angle difference between the direction of the touch points
let deltaAngle = Math.abs(touch.angle - prevTouch.angle); let deltaAngle = Math.abs(touch.angle - prevTouch.angle);
@ -458,6 +495,15 @@ export default class GestureHandler {
detail['clientX'] = pos.x; detail['clientX'] = pos.x;
detail['clientY'] = pos.y; detail['clientY'] = pos.y;
if (this._touchpadMode &&
this._tracked.length === 1) {
const touch = this._tracked[0];
detail['movementX'] = touch.movementX;
detail['movementY'] = touch.movementY;
}
// FIXME: other coordinates? // FIXME: other coordinates?
// Some gestures also have a magnitude // Some gestures also have a magnitude

View File

@ -194,6 +194,10 @@ export default class RFB extends EventTargetMixin {
this._gestureFirstDoubleTapEv = null; this._gestureFirstDoubleTapEv = null;
this._gestureLastMagnitudeX = 0; this._gestureLastMagnitudeX = 0;
this._gestureLastMagnitudeY = 0; this._gestureLastMagnitudeY = 0;
this._isTouchpadDragging = false;
this._touchpadTapTimeoutId = null;
this._lastTouchpadPinchMagnitude = 0;
this._currentPinchScale = 1;
// Bound event handlers // Bound event handlers
this._eventHandlers = { this._eventHandlers = {
@ -290,7 +294,7 @@ export default class RFB extends EventTargetMixin {
this._clippingViewport = false; this._clippingViewport = false;
this._scaleViewport = false; this._scaleViewport = false;
this._resizeSession = false; this._resizeSession = false;
this.touchpadMode = false; this._touchpadMode = false;
this._showDotCursor = false; this._showDotCursor = false;
if (options.showDotCursor !== undefined) { if (options.showDotCursor !== undefined) {
@ -410,6 +414,24 @@ export default class RFB extends EventTargetMixin {
} }
} }
/**
* @returns {boolean}
*/
get touchpadMode() {
return this._touchpadMode;
}
/**
* @param {boolean} enabled
*/
set touchpadMode(enabled) {
if (!this._gestures) {
return;
}
this._touchpadMode = enabled;
this._gestures.touchpadMode = enabled;
}
// ===== PUBLIC METHODS ===== // ===== PUBLIC METHODS =====
disconnect() { disconnect() {
@ -530,6 +552,17 @@ export default class RFB extends EventTargetMixin {
} }
} }
centerCursorInViewport() {
const container = document.getElementById('noVNC_container');
const containerBounds = container.getBoundingClientRect();
const x = containerBounds.left + (containerBounds.width * .5);
const y = containerBounds.top + (containerBounds.height * .5)
this._cursor.move(x, y);
const elementPos = clientToElement(x, y, this._canvas);
this._handleMouseMove(elementPos.x, elementPos.y);
}
getImageData() { getImageData() {
return this._display.getImageData(); return this._display.getImageData();
} }
@ -1264,21 +1297,57 @@ export default class RFB extends EventTargetMixin {
case 'gesturestart': case 'gesturestart':
switch (ev.detail.type) { switch (ev.detail.type) {
case 'onetap': case 'onetap':
if (this._touchpadMode) {
this._handleTouchpadOneTapEvent();
}
else {
this._handleTapEvent(ev, 0x1); this._handleTapEvent(ev, 0x1);
}
break; break;
case 'twotap': case 'twotap':
if (this._touchpadMode) {
this._sendTouchpadTwoTap();
break;
}
this._handleTapEvent(ev, 0x4); this._handleTapEvent(ev, 0x4);
break; break;
case 'threetap': case 'threetap':
if (this._touchpadMode) {
this._sendTouchpadThreeTap();
break;
}
this._handleTapEvent(ev, 0x2); this._handleTapEvent(ev, 0x2);
break; break;
case 'drag': case 'drag':
// In TouchpadMode, we don't want to move the cursor
// at the start of dragging. It should remain at its
// current location. We'll only press the left mouse
// button if this is the second tap in a double-tap
// sequence.
if (this._touchpadMode) {
if (this._touchpadTapTimeoutId > 0) {
this._clearTouchpadTapTimeoutId();
this._isTouchpadDragging = true;
this._mouseButtonMask = 0x1;
const cursorPos = this._getCursorPositionToCanvas();
this._sendMouse(cursorPos.x, cursorPos.y, 0x1);
}
break;
}
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, 0x1); this._handleMouseButton(pos.x, pos.y, true, 0x1);
break; break;
case 'longpress': case 'longpress':
// In TouchpadMode, we want to start the right-click at the
// current cursor location.
if (this._touchpadMode) {
const cursorPos = this._getCursorPositionToCanvas();
this._handleMouseButton(cursorPos.x, cursorPos.y, true, 0x4);
}
else {
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, 0x4); this._handleMouseButton(pos.x, pos.y, true, 0x4);
}
break; break;
case 'twodrag': case 'twodrag':
@ -1287,8 +1356,15 @@ export default class RFB extends EventTargetMixin {
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
break; break;
case 'pinch': case 'pinch':
this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX, magnitude = Math.hypot(
ev.detail.magnitudeX,
ev.detail.magnitudeY); ev.detail.magnitudeY);
if (this._touchpadMode) {
this._lastTouchpadPinchMagnitude = magnitude;
break;
}
this._gestureLastMagnitudeX = magnitude;
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
break; break;
} }
@ -1302,13 +1378,22 @@ export default class RFB extends EventTargetMixin {
break; break;
case 'drag': case 'drag':
case 'longpress': case 'longpress':
// In TouchpadMode, we want to move the cursor from its
// current position, not to where the touch currently is.
if (this._touchpadMode) {
this._handleTouchpadMove(ev.detail.movementX, ev.detail.movementY);
}
else {
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
}
break; break;
case 'twodrag': case 'twodrag':
// Always scroll in the same position. // Always scroll in the same position.
// We don't know if the mouse was moved so we need to move it // We don't know if the mouse was moved so we need to move it
// every update. // every update.
if (!this._touchpadMode) {
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
}
while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) { while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x8); this._handleMouseButton(pos.x, pos.y, true, 0x8);
this._handleMouseButton(pos.x, pos.y, false, 0x8); this._handleMouseButton(pos.x, pos.y, false, 0x8);
@ -1331,11 +1416,18 @@ export default class RFB extends EventTargetMixin {
} }
break; break;
case 'pinch': case 'pinch':
magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
if (this._touchpadMode) {
this._handleTouchpadPinchZoom(magnitude);
break;
}
// Always scroll in the same position. // Always scroll in the same position.
// We don't know if the mouse was moved so we need to move it // We don't know if the mouse was moved so we need to move it
// every update. // every update.
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
@ -1363,18 +1455,204 @@ export default class RFB extends EventTargetMixin {
case 'twodrag': case 'twodrag':
break; break;
case 'drag': case 'drag':
if (this._touchpadMode) {
if (this._isTouchpadDragging) {
this._mouseButtonMask = 0;
const cursorPos = this._getCursorPositionToCanvas();
this._sendMouse(cursorPos.x, cursorPos.y, 0);
this._isTouchpadDragging = false;
}
break;
}
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, false, 0x1); this._handleMouseButton(pos.x, pos.y, false, 0x1);
break; break;
case 'longpress': case 'longpress':
// In TouchPad mode, we want to finish at the current cursor location.
if (this._touchpadMode) {
const cursorPos = this._getCursorPositionToCanvas();
this._handleMouseButton(cursorPos.x, cursorPos.y, false, 0x4);
}
else {
this._fakeMouseMove(ev, pos.x, pos.y); this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, false, 0x4); this._handleMouseButton(pos.x, pos.y, false, 0x4);
}
break; break;
} }
break; break;
} }
} }
// TouchpadMode Private Methods
/**
* @param {number} movementX
* @param {number} movementY
*/
_handleTouchpadMove(movementX, movementY) {
// Add a multiplier to higher-velocity movements to
// traverse the screen quicker.
const xMultiplier = Math.max(5, Math.abs(movementX)) / 5;
movementX *= Math.min(xMultiplier, 4);
const yMultiplier = Math.max(5, Math.abs(movementY)) / 5;
movementY *= Math.min(yMultiplier, 4);
// Get the desired new location for the cursor.
let cursorPos = this._cursor.position;
let targetX = cursorPos.x + movementX;
let targetY = cursorPos.y + movementY;
// Constrain the location to the canvas bounds.
const canvasBounds = this._canvas.getBoundingClientRect();
const safeX = Math.max(canvasBounds.left, Math.min(targetX, canvasBounds.right));
const safeY = Math.max(canvasBounds.top, Math.min(targetY, canvasBounds.bottom));
// See if the cursor has moved outside the center deadzone.
const deadzone = this._getTouchpadCursorDeadZone();
const moveViewportX =
Math.min(safeX - deadzone.left, 0) +
Math.max(safeX - deadzone.right, 0);
const moveViewportY =
Math.min(safeY - deadzone.top, 0) +
Math.max(safeY - deadzone.bottom, 0);
// Try moving the viewport, getting the actual amount it moved.
const viewportChange = this._display.viewportTryMoveBy(moveViewportX, moveViewportY);
// Subtract the viewport position change from the target
// cursor position. This will cause it to stay at the
// edge of the deadzone if we're pushing against it, or
// move past it to the edge of the screen if the viewport
// can pan no further.
this._cursor.move(safeX - viewportChange.x, safeY - viewportChange.y);
// Finally, translate the coordinates to those relative to the
// canvas and send the pointer move event to the remote machine.
const posFromCanvas = clientToElement(safeX, safeY, this._canvas);
this._sendMouse(posFromCanvas.x, posFromCanvas.y, this._mouseButtonMask);
}
_handleTouchpadOneTapEvent() {
if (this._touchpadTapTimeoutId > 0) {
// A double-tap occurred.
this._clearTouchpadTapTimeoutId();
this._sendTouchpadTap();
this._sendTouchpadTap();
return;
}
this._touchpadTapTimeoutId = window.setTimeout(() => {
this._clearTouchpadTapTimeoutId();
this._sendTouchpadTap();
}, 250);
}
/**
*
* @param {number} magnitude
*/
_handleTouchpadPinchZoom(magnitude) {
if (this._lastTouchpadPinchMagnitude > 0) {
// Calculate the new pinch scale.
const container = document.getElementById('noVNC_container');
const magnitudeChange = this._lastTouchpadPinchMagnitude / magnitude;
const newScale = this._currentPinchScale * magnitudeChange;
this._currentPinchScale = Math.max(.25, Math.min(4, newScale));
// Capture the current viewport size.
const originalVpW = this._display.viewportLocation.w;
const originalVpH = this._display.viewportLocation.h;
// Change viewport size based on new scale.
const newWidth = container.clientWidth * this._currentPinchScale;
const newHeight = container.clientHeight * this._currentPinchScale;
this._display.viewportChangeSize(newWidth, newHeight);
// Apply scaling to CSS.
const visualScale = container.clientWidth / newWidth;
this._display.rescale(visualScale);
// Adjust viewport location to keep it centered.
const moveX = (originalVpW - this._display.viewportLocation.w) / 2;
const moveY = (originalVpH - this._display.viewportLocation.h) / 2;
this._display.viewportChangePos(moveX, moveY);
}
this._lastTouchpadPinchMagnitude = magnitude;
}
_clearTouchpadTapTimeoutId() {
window.clearTimeout(this._touchpadTapTimeoutId);
this._touchpadTapTimeoutId = 0;
}
_sendTouchpadTap() {
const cursorPos = this._getCursorPositionToCanvas();
this._sendMouse(cursorPos.x, cursorPos.y, 0x1);
this._sendMouse(cursorPos.x, cursorPos.y, 0);
this._mouseButtonMask = 0;
}
_sendTouchpadTwoTap() {
this._clearTouchpadTapTimeoutId();
const cursorPos = this._getCursorPositionToCanvas();
this._sendMouse(cursorPos.x, cursorPos.y, 0x4);
this._sendMouse(cursorPos.x, cursorPos.y, 0);
this._mouseButtonMask = 0;
}
_sendTouchpadThreeTap() {
this._clearTouchpadTapTimeoutId();
const cursorPos = this._getCursorPositionToCanvas();
this._sendMouse(cursorPos.x, cursorPos.y, 0x2);
this._sendMouse(cursorPos.x, cursorPos.y, 0);
this._mouseButtonMask = 0;
}
/**
* Gets the current cursor position, offset by the canvas client bounds.
* @returns {{x: number, y: number}}
*/
_getCursorPositionToCanvas() {
const cursorPos = this._cursor.position;
return clientToElement(cursorPos.x, cursorPos.y, this._canvas);
}
/**
* Returns the center area within the canvas bounds where
* cursor movement won't trigger viewport movement.
* @returns {{
* top: number,
* bottom: number,
* left: number,
* right: number,
* width: number,
* height: number
* }}
*/
_getTouchpadCursorDeadZone() {
const canvasBounds = this._canvas.getBoundingClientRect();
const canvasCenter = {
x: canvasBounds.width * .5,
y: canvasBounds.height * .5
}
const xFromCenter = canvasBounds.width * .1;
const yFromCenter = canvasBounds.height * .1;
const innerWidth = xFromCenter * 2;
const innerHeight = yFromCenter * 2;
return {
top: canvasCenter.y - yFromCenter,
bottom: canvasCenter.y + yFromCenter,
height: innerHeight,
left: canvasCenter.x - xFromCenter,
right: canvasCenter.x + xFromCenter,
width: innerWidth
}
}
// Message Handlers // Message Handlers
_negotiateProtocolVersion() { _negotiateProtocolVersion() {

View File

@ -11,17 +11,26 @@
import * as Log from './logging.js'; import * as Log from './logging.js';
// Touch detection // Touch detection
export let isTouchDevice = ('ontouchstart' in document.documentElement) || let _touchEventOccurred = false;
window.addEventListener('touchstart', function onFirstTouch() {
_touchEventOccurred = true;
window.removeEventListener('touchstart', onFirstTouch, false);
}, false);
// This needs to be a function to allow the exported value
// to update if touchstart event fires. Also, the other
// values are dynamic and can change without a page reload
// (e.g. opening the emulator in dev tools), so we don't want
// to assign them to a variable that captures their current value.
export function isTouchDevice() {
return _touchEventOccurred ||
('ontouchstart' in document.documentElement) ||
// requried for Chrome debugger // requried for Chrome debugger
(document.ontouchstart !== undefined) || (document.ontouchstart !== undefined) ||
// required for MS Surface // required for MS Surface
(navigator.maxTouchPoints > 0) || (navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0); (navigator.msMaxTouchPoints > 0);
window.addEventListener('touchstart', function onFirstTouch() { };
isTouchDevice = true;
window.removeEventListener('touchstart', onFirstTouch, false);
}, false);
// The goal is to find a certain physical width, the devicePixelRatio // The goal is to find a certain physical width, the devicePixelRatio
// brings us a bit closer but is not optimal. // brings us a bit closer but is not optimal.

View File

@ -6,7 +6,7 @@
import { supportsCursorURIs, isTouchDevice } from './browser.js'; import { supportsCursorURIs, isTouchDevice } from './browser.js';
const useFallback = !supportsCursorURIs || isTouchDevice; const useFallback = () => !supportsCursorURIs || isTouchDevice();
export default class Cursor { export default class Cursor {
constructor() { constructor() {
@ -14,7 +14,7 @@ export default class Cursor {
this._canvas = document.createElement('canvas'); this._canvas = document.createElement('canvas');
if (useFallback) { if (useFallback()) {
this._canvas.style.position = 'fixed'; this._canvas.style.position = 'fixed';
this._canvas.style.zIndex = '65535'; this._canvas.style.zIndex = '65535';
this._canvas.style.pointerEvents = 'none'; this._canvas.style.pointerEvents = 'none';
@ -37,6 +37,13 @@ export default class Cursor {
}; };
} }
/**
* @returns {{ x: number, y: number }}
*/
get position() {
return this._position;
}
attach(target) { attach(target) {
if (this._target) { if (this._target) {
this.detach(); this.detach();
@ -44,7 +51,7 @@ export default class Cursor {
this._target = target; this._target = target;
if (useFallback) { if (useFallback()) {
document.body.appendChild(this._canvas); document.body.appendChild(this._canvas);
const options = { capture: true, passive: true }; const options = { capture: true, passive: true };
@ -62,7 +69,7 @@ export default class Cursor {
return; return;
} }
if (useFallback) { if (useFallback()) {
const options = { capture: true, passive: true }; const options = { capture: true, passive: true };
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options); this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
@ -95,7 +102,7 @@ export default class Cursor {
ctx.clearRect(0, 0, w, h); ctx.clearRect(0, 0, w, h);
ctx.putImageData(img, 0, 0); ctx.putImageData(img, 0, 0);
if (useFallback) { if (useFallback()) {
this._updatePosition(); this._updatePosition();
} else { } else {
let url = this._canvas.toDataURL(); let url = this._canvas.toDataURL();
@ -116,7 +123,7 @@ export default class Cursor {
// Mouse events might be emulated, this allows // Mouse events might be emulated, this allows
// moving the cursor in such cases // moving the cursor in such cases
move(clientX, clientY) { move(clientX, clientY) {
if (!useFallback) { if (!useFallback()) {
return; return;
} }
// clientX/clientY are relative the _visual viewport_, // clientX/clientY are relative the _visual viewport_,