]>
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'; | |
23 | document.body.appendChild(this._canvas); | |
24 | } | |
b475eed5 | 25 | |
0e4808bf JD |
26 | this._position = { x: 0, y: 0 }; |
27 | this._hotSpot = { x: 0, y: 0 }; | |
28 | ||
29 | this._eventHandlers = { | |
30 | 'mouseover': this._handleMouseOver.bind(this), | |
31 | 'mouseleave': this._handleMouseLeave.bind(this), | |
32 | 'mousemove': this._handleMouseMove.bind(this), | |
33 | 'mouseup': this._handleMouseUp.bind(this), | |
34 | 'touchstart': this._handleTouchStart.bind(this), | |
35 | 'touchmove': this._handleTouchMove.bind(this), | |
36 | 'touchend': this._handleTouchEnd.bind(this), | |
37 | }; | |
38 | } | |
39 | ||
40 | attach(target) { | |
b475eed5 PO |
41 | if (this._target) { |
42 | this.detach(); | |
43 | } | |
44 | ||
45 | this._target = target; | |
46 | ||
baa4f23e PO |
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 | ||
b475eed5 | 63 | this.clear(); |
0e4808bf | 64 | } |
b475eed5 | 65 | |
0e4808bf | 66 | detach() { |
baa4f23e PO |
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 | ||
b475eed5 | 79 | this._target = null; |
0e4808bf | 80 | } |
b475eed5 | 81 | |
d1314d4b | 82 | change(rgba, hotx, hoty, w, h) { |
b475eed5 PO |
83 | if ((w === 0) || (h === 0)) { |
84 | this.clear(); | |
85 | return; | |
86 | } | |
87 | ||
baa4f23e PO |
88 | this._position.x = this._position.x + this._hotSpot.x - hotx; |
89 | this._position.y = this._position.y + this._hotSpot.y - hoty; | |
90 | this._hotSpot.x = hotx; | |
91 | this._hotSpot.y = hoty; | |
92 | ||
93 | let ctx = this._canvas.getContext('2d'); | |
b475eed5 | 94 | |
baa4f23e PO |
95 | this._canvas.width = w; |
96 | this._canvas.height = h; | |
b475eed5 PO |
97 | |
98 | let img; | |
99 | try { | |
100 | // IE doesn't support this | |
d1314d4b | 101 | img = new ImageData(new Uint8ClampedArray(rgba), w, h); |
b475eed5 PO |
102 | } catch (ex) { |
103 | img = ctx.createImageData(w, h); | |
d1314d4b | 104 | img.data.set(new Uint8ClampedArray(rgba)); |
b475eed5 PO |
105 | } |
106 | ctx.clearRect(0, 0, w, h); | |
107 | ctx.putImageData(img, 0, 0); | |
108 | ||
baa4f23e PO |
109 | if (useFallback) { |
110 | this._updatePosition(); | |
111 | } else { | |
112 | let url = this._canvas.toDataURL(); | |
113 | this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; | |
114 | } | |
0e4808bf | 115 | } |
b475eed5 | 116 | |
0e4808bf | 117 | clear() { |
b475eed5 | 118 | this._target.style.cursor = 'none'; |
baa4f23e PO |
119 | this._canvas.width = 0; |
120 | this._canvas.height = 0; | |
121 | this._position.x = this._position.x + this._hotSpot.x; | |
122 | this._position.y = this._position.y + this._hotSpot.y; | |
123 | this._hotSpot.x = 0; | |
124 | this._hotSpot.y = 0; | |
0e4808bf | 125 | } |
baa4f23e | 126 | |
0e4808bf | 127 | _handleMouseOver(event) { |
baa4f23e PO |
128 | // This event could be because we're entering the target, or |
129 | // moving around amongst its sub elements. Let the move handler | |
130 | // sort things out. | |
131 | this._handleMouseMove(event); | |
0e4808bf | 132 | } |
baa4f23e | 133 | |
0e4808bf | 134 | _handleMouseLeave(event) { |
baa4f23e | 135 | this._hideCursor(); |
0e4808bf | 136 | } |
baa4f23e | 137 | |
0e4808bf | 138 | _handleMouseMove(event) { |
baa4f23e PO |
139 | this._updateVisibility(event.target); |
140 | ||
141 | this._position.x = event.clientX - this._hotSpot.x; | |
142 | this._position.y = event.clientY - this._hotSpot.y; | |
143 | ||
144 | this._updatePosition(); | |
0e4808bf | 145 | } |
baa4f23e | 146 | |
0e4808bf | 147 | _handleMouseUp(event) { |
baa4f23e PO |
148 | // We might get this event because of a drag operation that |
149 | // moved outside of the target. Check what's under the cursor | |
150 | // now and adjust visibility based on that. | |
151 | let target = document.elementFromPoint(event.clientX, event.clientY); | |
152 | this._updateVisibility(target); | |
0e4808bf | 153 | } |
baa4f23e | 154 | |
0e4808bf | 155 | _handleTouchStart(event) { |
baa4f23e PO |
156 | // Just as for mouseover, we let the move handler deal with it |
157 | this._handleTouchMove(event); | |
0e4808bf | 158 | } |
baa4f23e | 159 | |
0e4808bf | 160 | _handleTouchMove(event) { |
baa4f23e PO |
161 | this._updateVisibility(event.target); |
162 | ||
163 | this._position.x = event.changedTouches[0].clientX - this._hotSpot.x; | |
164 | this._position.y = event.changedTouches[0].clientY - this._hotSpot.y; | |
165 | ||
166 | this._updatePosition(); | |
0e4808bf | 167 | } |
baa4f23e | 168 | |
0e4808bf | 169 | _handleTouchEnd(event) { |
baa4f23e PO |
170 | // Same principle as for mouseup |
171 | let target = document.elementFromPoint(event.changedTouches[0].clientX, | |
172 | event.changedTouches[0].clientY); | |
173 | this._updateVisibility(target); | |
0e4808bf | 174 | } |
baa4f23e | 175 | |
0e4808bf | 176 | _showCursor() { |
426a8c92 | 177 | if (this._canvas.style.visibility === 'hidden') { |
baa4f23e | 178 | this._canvas.style.visibility = ''; |
426a8c92 | 179 | } |
0e4808bf | 180 | } |
baa4f23e | 181 | |
0e4808bf | 182 | _hideCursor() { |
426a8c92 | 183 | if (this._canvas.style.visibility !== 'hidden') { |
baa4f23e | 184 | this._canvas.style.visibility = 'hidden'; |
426a8c92 | 185 | } |
0e4808bf | 186 | } |
baa4f23e PO |
187 | |
188 | // Should we currently display the cursor? | |
189 | // (i.e. are we over the target, or a child of the target without a | |
190 | // different cursor set) | |
0e4808bf | 191 | _shouldShowCursor(target) { |
baa4f23e | 192 | // Easy case |
426a8c92 | 193 | if (target === this._target) { |
baa4f23e | 194 | return true; |
426a8c92 | 195 | } |
baa4f23e | 196 | // Other part of the DOM? |
426a8c92 | 197 | if (!this._target.contains(target)) { |
baa4f23e | 198 | return false; |
426a8c92 | 199 | } |
baa4f23e PO |
200 | // Has the child its own cursor? |
201 | // FIXME: How can we tell that a sub element has an | |
202 | // explicit "cursor: none;"? | |
426a8c92 | 203 | if (window.getComputedStyle(target).cursor !== 'none') { |
baa4f23e | 204 | return false; |
426a8c92 | 205 | } |
baa4f23e | 206 | return true; |
0e4808bf | 207 | } |
baa4f23e | 208 | |
0e4808bf | 209 | _updateVisibility(target) { |
426a8c92 | 210 | if (this._shouldShowCursor(target)) { |
baa4f23e | 211 | this._showCursor(); |
426a8c92 | 212 | } else { |
baa4f23e | 213 | this._hideCursor(); |
426a8c92 | 214 | } |
0e4808bf | 215 | } |
baa4f23e | 216 | |
0e4808bf | 217 | _updatePosition() { |
baa4f23e PO |
218 | this._canvas.style.left = this._position.x + "px"; |
219 | this._canvas.style.top = this._position.y + "px"; | |
0e4808bf JD |
220 | } |
221 | } |