]>
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 | 89 | |
27496941 | 90 | let img = new ImageData(new Uint8ClampedArray(rgba), w, h); |
b475eed5 PO |
91 | ctx.clearRect(0, 0, w, h); |
92 | ctx.putImageData(img, 0, 0); | |
93 | ||
baa4f23e PO |
94 | if (useFallback) { |
95 | this._updatePosition(); | |
96 | } else { | |
97 | let url = this._canvas.toDataURL(); | |
98 | this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; | |
99 | } | |
0e4808bf | 100 | } |
b475eed5 | 101 | |
0e4808bf | 102 | clear() { |
b475eed5 | 103 | this._target.style.cursor = 'none'; |
baa4f23e PO |
104 | this._canvas.width = 0; |
105 | this._canvas.height = 0; | |
106 | this._position.x = this._position.x + this._hotSpot.x; | |
107 | this._position.y = this._position.y + this._hotSpot.y; | |
108 | this._hotSpot.x = 0; | |
109 | this._hotSpot.y = 0; | |
0e4808bf | 110 | } |
baa4f23e | 111 | |
32ed7c67 SM |
112 | // Mouse events might be emulated, this allows |
113 | // moving the cursor in such cases | |
114 | move(clientX, clientY) { | |
115 | if (!useFallback) { | |
116 | return; | |
117 | } | |
48f15efa PO |
118 | // clientX/clientY are relative the _visual viewport_, |
119 | // but our position is relative the _layout viewport_, | |
120 | // so try to compensate when we can | |
121 | if (window.visualViewport) { | |
122 | this._position.x = clientX + window.visualViewport.offsetLeft; | |
123 | this._position.y = clientY + window.visualViewport.offsetTop; | |
124 | } else { | |
125 | this._position.x = clientX; | |
126 | this._position.y = clientY; | |
127 | } | |
32ed7c67 SM |
128 | this._updatePosition(); |
129 | let target = document.elementFromPoint(clientX, clientY); | |
130 | this._updateVisibility(target); | |
131 | } | |
132 | ||
0e4808bf | 133 | _handleMouseOver(event) { |
baa4f23e PO |
134 | // This event could be because we're entering the target, or |
135 | // moving around amongst its sub elements. Let the move handler | |
136 | // sort things out. | |
137 | this._handleMouseMove(event); | |
0e4808bf | 138 | } |
baa4f23e | 139 | |
0e4808bf | 140 | _handleMouseLeave(event) { |
93869037 SM |
141 | // Check if we should show the cursor on the element we are leaving to |
142 | this._updateVisibility(event.relatedTarget); | |
0e4808bf | 143 | } |
baa4f23e | 144 | |
0e4808bf | 145 | _handleMouseMove(event) { |
baa4f23e PO |
146 | this._updateVisibility(event.target); |
147 | ||
148 | this._position.x = event.clientX - this._hotSpot.x; | |
149 | this._position.y = event.clientY - this._hotSpot.y; | |
150 | ||
151 | this._updatePosition(); | |
0e4808bf | 152 | } |
baa4f23e | 153 | |
0e4808bf | 154 | _handleMouseUp(event) { |
baa4f23e PO |
155 | // We might get this event because of a drag operation that |
156 | // moved outside of the target. Check what's under the cursor | |
157 | // now and adjust visibility based on that. | |
158 | let target = document.elementFromPoint(event.clientX, event.clientY); | |
159 | this._updateVisibility(target); | |
7a96fc37 SM |
160 | |
161 | // Captures end with a mouseup but we can't know the event order of | |
162 | // mouseup vs releaseCapture. | |
163 | // | |
164 | // In the cases when releaseCapture comes first, the code above is | |
165 | // enough. | |
166 | // | |
167 | // In the cases when the mouseup comes first, we need wait for the | |
168 | // browser to flush all events and then check again if the cursor | |
169 | // should be visible. | |
170 | if (this._captureIsActive()) { | |
171 | window.setTimeout(() => { | |
484a9551 PO |
172 | // We might have detached at this point |
173 | if (!this._target) { | |
174 | return; | |
175 | } | |
7a96fc37 SM |
176 | // Refresh the target from elementFromPoint since queued events |
177 | // might have altered the DOM | |
178 | target = document.elementFromPoint(event.clientX, | |
179 | event.clientY); | |
180 | this._updateVisibility(target); | |
181 | }, 0); | |
182 | } | |
0e4808bf | 183 | } |
baa4f23e | 184 | |
0e4808bf | 185 | _showCursor() { |
426a8c92 | 186 | if (this._canvas.style.visibility === 'hidden') { |
baa4f23e | 187 | this._canvas.style.visibility = ''; |
426a8c92 | 188 | } |
0e4808bf | 189 | } |
baa4f23e | 190 | |
0e4808bf | 191 | _hideCursor() { |
426a8c92 | 192 | if (this._canvas.style.visibility !== 'hidden') { |
baa4f23e | 193 | this._canvas.style.visibility = 'hidden'; |
426a8c92 | 194 | } |
0e4808bf | 195 | } |
baa4f23e PO |
196 | |
197 | // Should we currently display the cursor? | |
198 | // (i.e. are we over the target, or a child of the target without a | |
199 | // different cursor set) | |
0e4808bf | 200 | _shouldShowCursor(target) { |
c3a7524c SM |
201 | if (!target) { |
202 | return false; | |
203 | } | |
baa4f23e | 204 | // Easy case |
426a8c92 | 205 | if (target === this._target) { |
baa4f23e | 206 | return true; |
426a8c92 | 207 | } |
baa4f23e | 208 | // Other part of the DOM? |
426a8c92 | 209 | if (!this._target.contains(target)) { |
baa4f23e | 210 | return false; |
426a8c92 | 211 | } |
baa4f23e PO |
212 | // Has the child its own cursor? |
213 | // FIXME: How can we tell that a sub element has an | |
214 | // explicit "cursor: none;"? | |
426a8c92 | 215 | if (window.getComputedStyle(target).cursor !== 'none') { |
baa4f23e | 216 | return false; |
426a8c92 | 217 | } |
baa4f23e | 218 | return true; |
0e4808bf | 219 | } |
baa4f23e | 220 | |
0e4808bf | 221 | _updateVisibility(target) { |
7a96fc37 SM |
222 | // When the cursor target has capture we want to show the cursor. |
223 | // So, if a capture is active - look at the captured element instead. | |
224 | if (this._captureIsActive()) { | |
0c4b3e80 | 225 | target = document.captureElement; |
7a96fc37 | 226 | } |
426a8c92 | 227 | if (this._shouldShowCursor(target)) { |
baa4f23e | 228 | this._showCursor(); |
426a8c92 | 229 | } else { |
baa4f23e | 230 | this._hideCursor(); |
426a8c92 | 231 | } |
0e4808bf | 232 | } |
baa4f23e | 233 | |
0e4808bf | 234 | _updatePosition() { |
baa4f23e PO |
235 | this._canvas.style.left = this._position.x + "px"; |
236 | this._canvas.style.top = this._position.y + "px"; | |
0e4808bf | 237 | } |
7a96fc37 SM |
238 | |
239 | _captureIsActive() { | |
0c4b3e80 SM |
240 | return document.captureElement && |
241 | document.documentElement.contains(document.captureElement); | |
7a96fc37 | 242 | } |
0e4808bf | 243 | } |