]> git.proxmox.com Git - mirror_novnc.git/blob - core/util/cursor.js
da72723b395485c4665ce2456d98a092b2ce4d90
[mirror_novnc.git] / core / util / cursor.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright 2018 Pierre Ossman for noVNC
4 * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
5 */
6
7 import { supportsCursorURIs, isTouchDevice } from './browser.js';
8
9 const useFallback = !supportsCursorURIs() || isTouchDevice;
10
11 function Cursor(container) {
12 this._target = null;
13
14 this._canvas = document.createElement('canvas');
15
16 if (useFallback) {
17 this._canvas.style.position = 'fixed';
18 this._canvas.style.zIndex = '65535';
19 this._canvas.style.pointerEvents = 'none';
20 // Can't use "display" because of Firefox bug #1445997
21 this._canvas.style.visibility = 'hidden';
22 document.body.appendChild(this._canvas);
23 }
24
25 this._position = { x: 0, y: 0 };
26 this._hotSpot = { x: 0, y: 0 };
27
28 this._eventHandlers = {
29 'mouseover': this._handleMouseOver.bind(this),
30 'mouseleave': this._handleMouseLeave.bind(this),
31 'mousemove': this._handleMouseMove.bind(this),
32 'mouseup': this._handleMouseUp.bind(this),
33 'touchstart': this._handleTouchStart.bind(this),
34 'touchmove': this._handleTouchMove.bind(this),
35 'touchend': this._handleTouchEnd.bind(this),
36 };
37 }
38
39 Cursor.prototype = {
40 attach: function (target) {
41 if (this._target) {
42 this.detach();
43 }
44
45 this._target = target;
46
47 if (useFallback) {
48 // FIXME: These don't fire properly except for mouse
49 /// movement in IE. We want to also capture element
50 // movement, size changes, visibility, etc.
51 const options = { capture: true, passive: true };
52 this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
53 this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
54 this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
55 this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
56
57 // There is no "touchleave" so we monitor touchstart globally
58 window.addEventListener('touchstart', this._eventHandlers.touchstart, options);
59 this._target.addEventListener('touchmove', this._eventHandlers.touchmove, options);
60 this._target.addEventListener('touchend', this._eventHandlers.touchend, options);
61 }
62
63 this.clear();
64 },
65
66 detach: function () {
67 if (useFallback) {
68 const options = { capture: true, passive: true };
69 this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
70 this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
71 this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
72 this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
73
74 window.removeEventListener('touchstart', this._eventHandlers.touchstart, options);
75 this._target.removeEventListener('touchmove', this._eventHandlers.touchmove, options);
76 this._target.removeEventListener('touchend', this._eventHandlers.touchend, options);
77 }
78
79 this._target = null;
80 },
81
82 change: function (pixels, mask, hotx, hoty, w, h) {
83 if ((w === 0) || (h === 0)) {
84 this.clear();
85 return;
86 }
87
88 let cur = []
89 for (let y = 0; y < h; y++) {
90 for (let x = 0; x < w; x++) {
91 let idx = y * Math.ceil(w / 8) + Math.floor(x / 8);
92 let alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
93 idx = ((w * y) + x) * 4;
94 cur.push(pixels[idx + 2]); // red
95 cur.push(pixels[idx + 1]); // green
96 cur.push(pixels[idx]); // blue
97 cur.push(alpha); // alpha
98 }
99 }
100
101 this._position.x = this._position.x + this._hotSpot.x - hotx;
102 this._position.y = this._position.y + this._hotSpot.y - hoty;
103 this._hotSpot.x = hotx;
104 this._hotSpot.y = hoty;
105
106 let ctx = this._canvas.getContext('2d');
107
108 this._canvas.width = w;
109 this._canvas.height = h;
110
111 let img;
112 try {
113 // IE doesn't support this
114 img = new ImageData(new Uint8ClampedArray(cur), w, h);
115 } catch (ex) {
116 img = ctx.createImageData(w, h);
117 img.data.set(new Uint8ClampedArray(cur));
118 }
119 ctx.clearRect(0, 0, w, h);
120 ctx.putImageData(img, 0, 0);
121
122 if (useFallback) {
123 this._updatePosition();
124 } else {
125 let url = this._canvas.toDataURL();
126 this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
127 }
128 },
129
130 clear: function () {
131 this._target.style.cursor = 'none';
132 this._canvas.width = 0;
133 this._canvas.height = 0;
134 this._position.x = this._position.x + this._hotSpot.x;
135 this._position.y = this._position.y + this._hotSpot.y;
136 this._hotSpot.x = 0;
137 this._hotSpot.y = 0;
138 },
139
140 _handleMouseOver: function (event) {
141 // This event could be because we're entering the target, or
142 // moving around amongst its sub elements. Let the move handler
143 // sort things out.
144 this._handleMouseMove(event);
145 },
146
147 _handleMouseLeave: function (event) {
148 this._hideCursor();
149 },
150
151 _handleMouseMove: function (event) {
152 this._updateVisibility(event.target);
153
154 this._position.x = event.clientX - this._hotSpot.x;
155 this._position.y = event.clientY - this._hotSpot.y;
156
157 this._updatePosition();
158 },
159
160 _handleMouseUp: function (event) {
161 // We might get this event because of a drag operation that
162 // moved outside of the target. Check what's under the cursor
163 // now and adjust visibility based on that.
164 let target = document.elementFromPoint(event.clientX, event.clientY);
165 this._updateVisibility(target);
166 },
167
168 _handleTouchStart: function (event) {
169 // Just as for mouseover, we let the move handler deal with it
170 this._handleTouchMove(event);
171 },
172
173 _handleTouchMove: function (event) {
174 this._updateVisibility(event.target);
175
176 this._position.x = event.changedTouches[0].clientX - this._hotSpot.x;
177 this._position.y = event.changedTouches[0].clientY - this._hotSpot.y;
178
179 this._updatePosition();
180 },
181
182 _handleTouchEnd: function (event) {
183 // Same principle as for mouseup
184 let target = document.elementFromPoint(event.changedTouches[0].clientX,
185 event.changedTouches[0].clientY);
186 this._updateVisibility(target);
187 },
188
189 _showCursor: function () {
190 if (this._canvas.style.visibility === 'hidden')
191 this._canvas.style.visibility = '';
192 },
193
194 _hideCursor: function () {
195 if (this._canvas.style.visibility !== 'hidden')
196 this._canvas.style.visibility = 'hidden';
197 },
198
199 // Should we currently display the cursor?
200 // (i.e. are we over the target, or a child of the target without a
201 // different cursor set)
202 _shouldShowCursor: function (target) {
203 // Easy case
204 if (target === this._target)
205 return true;
206 // Other part of the DOM?
207 if (!this._target.contains(target))
208 return false;
209 // Has the child its own cursor?
210 // FIXME: How can we tell that a sub element has an
211 // explicit "cursor: none;"?
212 if (window.getComputedStyle(target).cursor !== 'none')
213 return false;
214 return true;
215 },
216
217 _updateVisibility: function (target) {
218 if (this._shouldShowCursor(target))
219 this._showCursor();
220 else
221 this._hideCursor();
222 },
223
224 _updatePosition: function () {
225 this._canvas.style.left = this._position.x + "px";
226 this._canvas.style.top = this._position.y + "px";
227 },
228 };
229
230 export default Cursor;