diff --git a/app/ui.js b/app/ui.js index a7fd03c5f..94ef2b548 100644 --- a/app/ui.js +++ b/app/ui.js @@ -88,7 +88,7 @@ const UI = { }); // Adapt the interface for touch screen devices - if (isTouchDevice) { + if (isTouchDevice()) { // Remove the address bar setTimeout(() => window.scrollTo(0, 1), 100); } @@ -464,6 +464,12 @@ const UI = { .classList.remove('noVNC_open'); }, + /** + * @param {string} text + * @param { "normal" | "info" | "warn" | "warning" | "error" } statusType + * @param {number} time + * @returns + */ showStatus(text, statusType, time) { const statusElem = document.getElementById('noVNC_status'); @@ -1064,8 +1070,10 @@ const UI = { UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); UI.rfb.compressionLevel = parseInt(UI.getSetting('compression')); UI.rfb.showDotCursor = UI.getSetting('show_dot'); + UI.rfb.touchpadMode = WebUtil.readSetting('touchpad_mode', 'false') === 'true'; UI.updateViewOnly(); // requires UI.rfb + UI.updateTouchpadMode(); }, disconnect() { @@ -1119,6 +1127,12 @@ const UI = { // Do this last because it can only be used on rendered elements 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) { @@ -1348,7 +1362,7 @@ const UI = { // Can't be clipping if viewport is scaled to fit UI.forceSetting('view_clip', false); UI.rfb.clipViewport = false; - } else if (brokenScrollbars) { + } else if (brokenScrollbars || UI.rfb.touchpadMode) { UI.forceSetting('view_clip', true); UI.rfb.clipViewport = true; } else { @@ -1372,6 +1386,7 @@ const UI = { UI.rfb.dragViewport = !UI.rfb.dragViewport; UI.updateViewDrag(); + UI.updateTouchpadMode(); }, updateViewDrag() { @@ -1379,6 +1394,10 @@ const UI = { const viewDragButton = document.getElementById('noVNC_view_drag_button'); + if (UI.rfb.dragViewport) { + UI.rfb.touchpadMode = false; + } + if ((!UI.rfb.clipViewport || !UI.rfb.clippingViewport) && UI.rfb.dragViewport) { // We are no longer clipping the viewport. Make sure @@ -1432,7 +1451,7 @@ const UI = { * ------v------*/ showVirtualKeyboard() { - if (!isTouchDevice) return; + if (!isTouchDevice()) return; const input = document.getElementById('noVNC_keyboardinput'); @@ -1450,7 +1469,7 @@ const UI = { }, hideVirtualKeyboard() { - if (!isTouchDevice) return; + if (!isTouchDevice()) return; const input = document.getElementById('noVNC_keyboardinput'); @@ -1599,12 +1618,33 @@ const UI = { if (!UI.rfb) return; UI.rfb.touchpadMode = !UI.rfb.touchpadMode; - UI.updateTouchpadButton(); + WebUtil.writeSetting('touchpad_mode', UI.rfb.touchpadMode); + UI.updateTouchpadMode(); + UI.updateViewDrag(); }, - updateTouchpadButton() { + updateTouchpadMode() { + 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) { touchpadButton.classList.add("noVNC_selected"); } else { @@ -1730,12 +1770,14 @@ const UI = { .classList.add('noVNC_hidden'); document.getElementById('noVNC_clipboard_button') .classList.add('noVNC_hidden'); + document.getElementById('noVNC_clipboard_button') + .classList.add('noVNC_hidden'); } else { document.getElementById('noVNC_keyboard_button') .classList.remove('noVNC_hidden'); document.getElementById('noVNC_toggle_extra_keys_button') .classList.remove('noVNC_hidden'); - document.getElementById('noVNC_clipboard_button') + document.getElementById('noVNC_touchpad_button') .classList.remove('noVNC_hidden'); } }, diff --git a/core/display.js b/core/display.js index fcd626999..c8704ebc3 100644 --- a/core/display.js +++ b/core/display.js @@ -87,8 +87,38 @@ export default class Display { return this._fbHeight; } + get viewportLocation() { + return this._viewportLoc; + } + // ===== 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) { const vp = this._viewportLoc; deltaX = Math.floor(deltaX); @@ -433,6 +463,10 @@ export default class Display { this._rescale(scaleRatio); } + rescale(factor) { + this._rescale(factor); + } + // ===== PRIVATE METHODS ===== _rescale(factor) { diff --git a/core/input/gesturehandler.js b/core/input/gesturehandler.js index 6fa72d2aa..73e681cc0 100644 --- a/core/input/gesturehandler.js +++ b/core/input/gesturehandler.js @@ -18,7 +18,6 @@ const GH_PINCH = 64; const GH_INITSTATE = 127; -const GH_MOVE_THRESHOLD = 50; const GH_ANGLE_THRESHOLD = 90; // Degrees // Timeout when waiting for gestures (ms) @@ -38,6 +37,7 @@ export default class GestureHandler { this._target = null; this._state = GH_INITSTATE; + this._touchpadMode = false; this._tracked = []; this._ignored = []; @@ -51,6 +51,37 @@ export default class GestureHandler { 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) { this.detach(); @@ -64,7 +95,6 @@ export default class GestureHandler { this._target.addEventListener('touchcancel', this._boundEventHandler); } - detach() { if (!this._target) { return; @@ -84,6 +114,10 @@ export default class GestureHandler { this._target = null; } + /** + * + * @param {TouchEvent} e + */ _eventHandler(e) { let fn; @@ -102,7 +136,6 @@ export default class GestureHandler { fn = this._touchEnd; break; } - for (let i = 0; i < e.changedTouches.length; i++) { let touch = e.changedTouches[i]; fn.call(this, touch.identifier, touch.clientX, touch.clientY); @@ -142,9 +175,11 @@ export default class GestureHandler { firstY: y, lastX: x, lastY: y, - angle: 0 + movementX: 0, + movementY: 0, + angle: 0, }); - + switch (this._tracked.length) { case 1: this._startLongpressTimeout(); @@ -164,7 +199,7 @@ export default class GestureHandler { } } - _touchMove(id, x, y) { + _touchMove(id, x, y) { let touch = this._tracked.find(t => t.id === id); // If this is an update for a touch we're not tracking, ignore it @@ -173,6 +208,8 @@ export default class GestureHandler { } // Update the touches last position with the event coordinates + touch.movementX = x - touch.lastX; + touch.movementY = y - touch.lastY; touch.lastX = x; touch.lastY = y; @@ -187,7 +224,7 @@ export default class GestureHandler { if (!this._hasDetectedGesture()) { // Ignore moves smaller than the minimum threshold - if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) { + if (Math.hypot(deltaX, deltaY) < this._ghMoveThreshold) { return; } @@ -216,7 +253,7 @@ export default class GestureHandler { // We know that the current touch moved far enough, // but unless both touches moved further than their // 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 let deltaAngle = Math.abs(touch.angle - prevTouch.angle); @@ -458,6 +495,15 @@ export default class GestureHandler { detail['clientX'] = pos.x; 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? // Some gestures also have a magnitude diff --git a/core/rfb.js b/core/rfb.js index 443fd9309..5dc8e95b7 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -188,12 +188,16 @@ export default class RFB extends EventTargetMixin { this._viewportHasMoved = false; this._accumulatedWheelDeltaX = 0; this._accumulatedWheelDeltaY = 0; - + // Gesture state this._gestureLastTapTime = null; this._gestureFirstDoubleTapEv = null; this._gestureLastMagnitudeX = 0; this._gestureLastMagnitudeY = 0; + this._isTouchpadDragging = false; + this._touchpadTapTimeoutId = null; + this._lastTouchpadPinchMagnitude = 0; + this._currentPinchScale = 1; // Bound event handlers this._eventHandlers = { @@ -290,7 +294,7 @@ export default class RFB extends EventTargetMixin { this._clippingViewport = false; this._scaleViewport = false; this._resizeSession = false; - this.touchpadMode = false; + this._touchpadMode = false; this._showDotCursor = false; if (options.showDotCursor !== undefined) { @@ -409,6 +413,24 @@ export default class RFB extends EventTargetMixin { this._sendEncodings(); } } + + /** + * @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 ===== @@ -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() { return this._display.getImageData(); } @@ -1264,21 +1297,57 @@ export default class RFB extends EventTargetMixin { case 'gesturestart': switch (ev.detail.type) { case 'onetap': - this._handleTapEvent(ev, 0x1); + if (this._touchpadMode) { + this._handleTouchpadOneTapEvent(); + } + else { + this._handleTapEvent(ev, 0x1); + } break; case 'twotap': + if (this._touchpadMode) { + this._sendTouchpadTwoTap(); + break; + } this._handleTapEvent(ev, 0x4); break; case 'threetap': + if (this._touchpadMode) { + this._sendTouchpadThreeTap(); + break; + } this._handleTapEvent(ev, 0x2); break; 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._handleMouseButton(pos.x, pos.y, true, 0x1); break; case 'longpress': - this._fakeMouseMove(ev, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, true, 0x4); + // 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._handleMouseButton(pos.x, pos.y, true, 0x4); + } break; case 'twodrag': @@ -1287,8 +1356,15 @@ export default class RFB extends EventTargetMixin { this._fakeMouseMove(ev, pos.x, pos.y); break; case 'pinch': - this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX, - ev.detail.magnitudeY); + magnitude = Math.hypot( + ev.detail.magnitudeX, + ev.detail.magnitudeY); + + if (this._touchpadMode) { + this._lastTouchpadPinchMagnitude = magnitude; + break; + } + this._gestureLastMagnitudeX = magnitude; this._fakeMouseMove(ev, pos.x, pos.y); break; } @@ -1302,13 +1378,22 @@ export default class RFB extends EventTargetMixin { break; case 'drag': case 'longpress': - this._fakeMouseMove(ev, pos.x, pos.y); + // 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); + } break; case 'twodrag': // Always scroll in the same position. // We don't know if the mouse was moved so we need to move it // every update. - this._fakeMouseMove(ev, pos.x, pos.y); + if (!this._touchpadMode) { + this._fakeMouseMove(ev, pos.x, pos.y); + } while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) { this._handleMouseButton(pos.x, pos.y, true, 0x8); this._handleMouseButton(pos.x, pos.y, false, 0x8); @@ -1331,11 +1416,18 @@ export default class RFB extends EventTargetMixin { } break; case 'pinch': + magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY); + + if (this._touchpadMode) { + this._handleTouchpadPinchZoom(magnitude); + break; + } + // Always scroll in the same position. // We don't know if the mouse was moved so we need to move it // every update. this._fakeMouseMove(ev, pos.x, pos.y); - magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY); + if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) { @@ -1363,18 +1455,204 @@ export default class RFB extends EventTargetMixin { case 'twodrag': break; 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._handleMouseButton(pos.x, pos.y, false, 0x1); break; case 'longpress': - this._fakeMouseMove(ev, pos.x, pos.y); - this._handleMouseButton(pos.x, pos.y, false, 0x4); + // 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._handleMouseButton(pos.x, pos.y, false, 0x4); + } 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 _negotiateProtocolVersion() { diff --git a/core/util/browser.js b/core/util/browser.js index bbc9f5c1e..1db35b5c8 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -11,17 +11,26 @@ import * as Log from './logging.js'; // Touch detection -export let isTouchDevice = ('ontouchstart' in document.documentElement) || - // requried for Chrome debugger - (document.ontouchstart !== undefined) || - // required for MS Surface - (navigator.maxTouchPoints > 0) || - (navigator.msMaxTouchPoints > 0); +let _touchEventOccurred = false; window.addEventListener('touchstart', function onFirstTouch() { - isTouchDevice = true; + _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 + (document.ontouchstart !== undefined) || + // required for MS Surface + (navigator.maxTouchPoints > 0) || + (navigator.msMaxTouchPoints > 0); +}; // The goal is to find a certain physical width, the devicePixelRatio // brings us a bit closer but is not optimal. diff --git a/core/util/cursor.js b/core/util/cursor.js index 3000cf0e6..0f8f86136 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -6,7 +6,7 @@ import { supportsCursorURIs, isTouchDevice } from './browser.js'; -const useFallback = !supportsCursorURIs || isTouchDevice; +const useFallback = () => !supportsCursorURIs || isTouchDevice(); export default class Cursor { constructor() { @@ -14,7 +14,7 @@ export default class Cursor { this._canvas = document.createElement('canvas'); - if (useFallback) { + if (useFallback()) { this._canvas.style.position = 'fixed'; this._canvas.style.zIndex = '65535'; 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) { if (this._target) { this.detach(); @@ -44,7 +51,7 @@ export default class Cursor { this._target = target; - if (useFallback) { + if (useFallback()) { document.body.appendChild(this._canvas); const options = { capture: true, passive: true }; @@ -62,7 +69,7 @@ export default class Cursor { return; } - if (useFallback) { + if (useFallback()) { const options = { capture: true, passive: true }; this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options); @@ -95,7 +102,7 @@ export default class Cursor { ctx.clearRect(0, 0, w, h); ctx.putImageData(img, 0, 0); - if (useFallback) { + if (useFallback()) { this._updatePosition(); } else { let url = this._canvas.toDataURL(); @@ -116,7 +123,7 @@ export default class Cursor { // Mouse events might be emulated, this allows // moving the cursor in such cases move(clientX, clientY) { - if (!useFallback) { + if (!useFallback()) { return; } // clientX/clientY are relative the _visual viewport_,