cursor.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /*
  2. * noVNC: HTML5 VNC client
  3. * Copyright (C) 2019 The noVNC Authors
  4. * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
  5. */
  6. import { supportsCursorURIs, isTouchDevice } from './browser.js';
  7. const useFallback = !supportsCursorURIs || isTouchDevice;
  8. export default class Cursor {
  9. constructor() {
  10. this._target = null;
  11. this._canvas = document.createElement('canvas');
  12. if (useFallback) {
  13. this._canvas.style.position = 'fixed';
  14. this._canvas.style.zIndex = '65535';
  15. this._canvas.style.pointerEvents = 'none';
  16. // Can't use "display" because of Firefox bug #1445997
  17. this._canvas.style.visibility = 'hidden';
  18. }
  19. this._position = { x: 0, y: 0 };
  20. this._hotSpot = { x: 0, y: 0 };
  21. this._eventHandlers = {
  22. 'mouseover': this._handleMouseOver.bind(this),
  23. 'mouseleave': this._handleMouseLeave.bind(this),
  24. 'mousemove': this._handleMouseMove.bind(this),
  25. 'mouseup': this._handleMouseUp.bind(this),
  26. };
  27. }
  28. attach(target) {
  29. if (this._target) {
  30. this.detach();
  31. }
  32. this._target = target;
  33. if (useFallback) {
  34. document.body.appendChild(this._canvas);
  35. const options = { capture: true, passive: true };
  36. this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
  37. this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
  38. this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
  39. this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
  40. }
  41. this.clear();
  42. }
  43. detach() {
  44. if (!this._target) {
  45. return;
  46. }
  47. if (useFallback) {
  48. const options = { capture: true, passive: true };
  49. this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
  50. this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
  51. this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
  52. this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
  53. document.body.removeChild(this._canvas);
  54. }
  55. this._target = null;
  56. }
  57. change(rgba, hotx, hoty, w, h) {
  58. if ((w === 0) || (h === 0)) {
  59. this.clear();
  60. return;
  61. }
  62. this._position.x = this._position.x + this._hotSpot.x - hotx;
  63. this._position.y = this._position.y + this._hotSpot.y - hoty;
  64. this._hotSpot.x = hotx;
  65. this._hotSpot.y = hoty;
  66. let ctx = this._canvas.getContext('2d');
  67. this._canvas.width = w;
  68. this._canvas.height = h;
  69. let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
  70. ctx.clearRect(0, 0, w, h);
  71. ctx.putImageData(img, 0, 0);
  72. if (useFallback) {
  73. this._updatePosition();
  74. } else {
  75. let url = this._canvas.toDataURL();
  76. this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
  77. }
  78. }
  79. clear() {
  80. this._target.style.cursor = 'none';
  81. this._canvas.width = 0;
  82. this._canvas.height = 0;
  83. this._position.x = this._position.x + this._hotSpot.x;
  84. this._position.y = this._position.y + this._hotSpot.y;
  85. this._hotSpot.x = 0;
  86. this._hotSpot.y = 0;
  87. }
  88. // Mouse events might be emulated, this allows
  89. // moving the cursor in such cases
  90. move(clientX, clientY) {
  91. if (!useFallback) {
  92. return;
  93. }
  94. // clientX/clientY are relative the _visual viewport_,
  95. // but our position is relative the _layout viewport_,
  96. // so try to compensate when we can
  97. if (window.visualViewport) {
  98. this._position.x = clientX + window.visualViewport.offsetLeft;
  99. this._position.y = clientY + window.visualViewport.offsetTop;
  100. } else {
  101. this._position.x = clientX;
  102. this._position.y = clientY;
  103. }
  104. this._updatePosition();
  105. let target = document.elementFromPoint(clientX, clientY);
  106. this._updateVisibility(target);
  107. }
  108. _handleMouseOver(event) {
  109. // This event could be because we're entering the target, or
  110. // moving around amongst its sub elements. Let the move handler
  111. // sort things out.
  112. this._handleMouseMove(event);
  113. }
  114. _handleMouseLeave(event) {
  115. // Check if we should show the cursor on the element we are leaving to
  116. this._updateVisibility(event.relatedTarget);
  117. }
  118. _handleMouseMove(event) {
  119. this._updateVisibility(event.target);
  120. this._position.x = event.clientX - this._hotSpot.x;
  121. this._position.y = event.clientY - this._hotSpot.y;
  122. this._updatePosition();
  123. }
  124. _handleMouseUp(event) {
  125. // We might get this event because of a drag operation that
  126. // moved outside of the target. Check what's under the cursor
  127. // now and adjust visibility based on that.
  128. let target = document.elementFromPoint(event.clientX, event.clientY);
  129. this._updateVisibility(target);
  130. // Captures end with a mouseup but we can't know the event order of
  131. // mouseup vs releaseCapture.
  132. //
  133. // In the cases when releaseCapture comes first, the code above is
  134. // enough.
  135. //
  136. // In the cases when the mouseup comes first, we need wait for the
  137. // browser to flush all events and then check again if the cursor
  138. // should be visible.
  139. if (this._captureIsActive()) {
  140. window.setTimeout(() => {
  141. // We might have detached at this point
  142. if (!this._target) {
  143. return;
  144. }
  145. // Refresh the target from elementFromPoint since queued events
  146. // might have altered the DOM
  147. target = document.elementFromPoint(event.clientX,
  148. event.clientY);
  149. this._updateVisibility(target);
  150. }, 0);
  151. }
  152. }
  153. _showCursor() {
  154. if (this._canvas.style.visibility === 'hidden') {
  155. this._canvas.style.visibility = '';
  156. }
  157. }
  158. _hideCursor() {
  159. if (this._canvas.style.visibility !== 'hidden') {
  160. this._canvas.style.visibility = 'hidden';
  161. }
  162. }
  163. // Should we currently display the cursor?
  164. // (i.e. are we over the target, or a child of the target without a
  165. // different cursor set)
  166. _shouldShowCursor(target) {
  167. if (!target) {
  168. return false;
  169. }
  170. // Easy case
  171. if (target === this._target) {
  172. return true;
  173. }
  174. // Other part of the DOM?
  175. if (!this._target.contains(target)) {
  176. return false;
  177. }
  178. // Has the child its own cursor?
  179. // FIXME: How can we tell that a sub element has an
  180. // explicit "cursor: none;"?
  181. if (window.getComputedStyle(target).cursor !== 'none') {
  182. return false;
  183. }
  184. return true;
  185. }
  186. _updateVisibility(target) {
  187. // When the cursor target has capture we want to show the cursor.
  188. // So, if a capture is active - look at the captured element instead.
  189. if (this._captureIsActive()) {
  190. target = document.captureElement;
  191. }
  192. if (this._shouldShowCursor(target)) {
  193. this._showCursor();
  194. } else {
  195. this._hideCursor();
  196. }
  197. }
  198. _updatePosition() {
  199. this._canvas.style.left = this._position.x + "px";
  200. this._canvas.style.top = this._position.y + "px";
  201. }
  202. _captureIsActive() {
  203. return document.captureElement &&
  204. document.documentElement.contains(document.captureElement);
  205. }
  206. }