diff options
author | Solly Ross <sross@redhat.com> | 2018-03-05 14:13:21 -0500 |
---|---|---|
committer | Solly Ross <sross@redhat.com> | 2018-03-05 14:17:36 -0500 |
commit | 97fbc2bee3a09ec1fac281516435d65369f773e1 (patch) | |
tree | ffae71d490d203526fca88799d55826548cb0111 | |
parent | e62b4ccb5ea4dccc51b7712981e4442dc54171c6 (diff) | |
download | novnc-feature/local-pseudo-cursor.tar.gz |
Render cursor manually when neededfeature/local-pseudo-cursor
Some browsers (*cough* IE *cough*) don't support cursors via data URIs,
and touch devices sometimes don't have proper cursors, so we had to rely
on remote cursor support in some cases.
Instead, we can now display local cursors by rendering the cursor to a
transparent canvas (with pointer events disabled on that mini-canvas)
when we encounter those conditions, meaning we can now advertise local
cursor support in all conditions.
Fixes #572.
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | app/styles/base.css | 6 | ||||
-rw-r--r-- | core/display.js | 52 | ||||
-rw-r--r-- | core/pseudo-cursor.js | 41 | ||||
-rw-r--r-- | core/rfb.js | 5 |
5 files changed, 92 insertions, 13 deletions
@@ -10,3 +10,4 @@ recordings *.swp *~ noVNC-*.tgz +/package-lock.json diff --git a/app/styles/base.css b/app/styles/base.css index 344db9b..cb9fce0 100644 --- a/app/styles/base.css +++ b/app/styles/base.css @@ -900,3 +900,9 @@ select:active { font-size: 90px; } } + +/* TODO: reorganize */ +.noVNC_pseudo_cursor { + position: fixed; + pointer-events: none; +} diff --git a/core/display.js b/core/display.js index 9915615..f91ff3b 100644 --- a/core/display.js +++ b/core/display.js @@ -9,6 +9,8 @@ import * as Log from './util/logging.js'; import Base64 from "./base64.js"; +import { supportsCursorURIs, isTouchDevice } from './util/browser.js'; +import PseudoCursor from './pseudo-cursor.js'; export default function Display(target) { this._drawCtx = null; @@ -66,6 +68,12 @@ export default function Display(target) { throw new Error("Canvas does not support createImageData"); } + // check cursor support to initialize the pseudo-cursor + this._psuedo_cursor = null; + if (!supportsCursorURIs() || isTouchDevice) { + this._pseudo_cursor = new PseudoCursor(this._target, this._target.parentElement); + } + this._tile16x16 = this._drawCtx.createImageData(16, 16); Log.Debug("<< Display.constructor"); }; @@ -498,8 +506,38 @@ Display.prototype = { this._damage(x, y, img.width, img.height); }, + moveCursor: function (x, y) { + // this is only relevant for pseudo-cursors + if (this._pseudo_cursor === null) { return; } + + // convert viewport-local coordinates back to real-space coordinates, + // relative to the canvas. + var realRelX = (x - this._viewportLoc.x) * this._scale; + var realRelY = (y - this._viewportLoc.y) * this._scale; + + this._pseudo_cursor.move(x, y); + }, + changeCursor: function (pixels, mask, hotx, hoty, w, h) { - Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h); + // in the case of a pseudo-cursor, just render it + // to the pseudo-cursor's canvas + if (this._pseudo_cursor !== null) { + this._pseudo_cursor.change(hotx, hoty, function (canvas) { + Display.renderCursor(canvas, pixels, mask, hotx, hoty, w, h); + }); + return; + } + + // otherwise, update our canvas's cursor via data URIs + if ((w === 0) || (h === 0)) { + target.style.cursor = 'none'; + return; + } + + var canvas = document.createElement('canvas'); + Display.renderCursor(canvas, pixels, mask, hotx, hoty, w, h); + var url = canvas.toDataURL(); + this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; }, defaultCursor: function () { @@ -657,9 +695,10 @@ Display.prototype = { }; // Class Methods -Display.changeCursor = function (target, pixels, mask, hotx, hoty, w, h) { +Display.renderCursor = function (canvas, pixels, mask, hotx, hoty, w, h) { + canvas.width = w; + canvas.height = w; if ((w === 0) || (h === 0)) { - target.style.cursor = 'none'; return; } @@ -677,12 +716,8 @@ Display.changeCursor = function (target, pixels, mask, hotx, hoty, w, h) { } } - var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); - canvas.width = w; - canvas.height = h; - var img; if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { img = new ImageData(new Uint8ClampedArray(cur), w, h); @@ -692,7 +727,4 @@ Display.changeCursor = function (target, pixels, mask, hotx, hoty, w, h) { } ctx.clearRect(0, 0, w, h); ctx.putImageData(img, 0, 0); - - var url = canvas.toDataURL(); - target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; }; diff --git a/core/pseudo-cursor.js b/core/pseudo-cursor.js new file mode 100644 index 0000000..e9bdc76 --- /dev/null +++ b/core/pseudo-cursor.js @@ -0,0 +1,41 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2018 the noVNC authors. + * Licensed under MPL 2.0 (see LICENSE.txt) + * + * See README.md for usage and integration instructions. + */ + +export default function PseudoCursor(boundingTarget, container) { + this._boundingTarget = boundingTarget; + this._canvas = document.createElement('canvas'); + this._canvas.classList.add('noVNC_pseudo_cursor'); + + this._container = container; + this._container.appendChild(this._canvas); + + this._drawCtx = this._canvas.getContext('2d'); + + this._hotX = 0; + this._hotY = 0; +} + +PseudoCursor.prototype = { + change: function(hotX, hotY, renderCallback) { + this._drawCtx.clearRect(0, 0, this._canvas.width, this._canvas.height); + renderCallback(this._canvas); + this._hotX = hotX; + this._hotY = hotY; + }, + move: function (relX, relY) { + // TODO: do we need to cache this to avoid lots of garbage? + var boundingRect = this._boundingTarget.getBoundingClientRect(); + + // get the absolute X and Y, factoring in the hotspot + var absX = boundingRect.left + relX - this._hotX; + var absY = boundingRect.top + relY - this._hotY; + + this._canvas.style.top = Math.floor(absY) + "px"; + this._canvas.style.left = Math.floor(absX) + "px"; + } +}; diff --git a/core/rfb.js b/core/rfb.js index 81e1e6a..cbffec4 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -12,7 +12,6 @@ import * as Log from './util/logging.js'; import { decodeUTF8 } from './util/strings.js'; -import { supportsCursorURIs, isTouchDevice } from './util/browser.js'; import EventTargetMixin from './util/eventtarget.js'; import Display from "./display.js"; import Keyboard from "./input/keyboard.js"; @@ -806,6 +805,7 @@ RFB.prototype = { if (this._rfb_connection_state !== 'connected') { return; } RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + this._display.moveCursor(this._display.absX(x), this._display.absY(y)); }, // Message Handlers @@ -1275,8 +1275,7 @@ RFB.prototype = { encs.push(encodings.pseudoEncodingFence); encs.push(encodings.pseudoEncodingContinuousUpdates); - if (supportsCursorURIs() && - !isTouchDevice && this._fb_depth == 24) { + if (this._fb_depth == 24) { encs.push(encodings.pseudoEncodingCursor); } |