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