]> git.proxmox.com Git - mirror_novnc.git/blob - core/display.js
Move cursor URI check to RFB object
[mirror_novnc.git] / core / display.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2015 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 (see LICENSE.txt)
6 *
7 * See README.md for usage and integration instructions.
8 */
9
10 /*jslint browser: true, white: false */
11 /*global Util, Base64, changeCursor */
12
13 import { set_defaults, make_properties } from './util/properties.js';
14 import * as Log from './util/logging.js';
15 import Base64 from "./base64.js";
16
17 export default function Display(target, defaults) {
18 this._drawCtx = null;
19 this._c_forceCanvas = false;
20
21 this._renderQ = []; // queue drawing actions for in-oder rendering
22 this._flushing = false;
23
24 // the full frame buffer (logical canvas) size
25 this._fb_width = 0;
26 this._fb_height = 0;
27
28 this._prevDrawStyle = "";
29 this._tile = null;
30 this._tile16x16 = null;
31 this._tile_x = 0;
32 this._tile_y = 0;
33
34 set_defaults(this, defaults, {
35 'scale': 1.0,
36 'viewport': false,
37 "onFlush": function () {},
38 });
39
40 Log.Debug(">> Display.constructor");
41
42 // The visible canvas
43 this._target = target;
44
45 if (!this._target) {
46 throw new Error("Target must be set");
47 }
48
49 if (typeof this._target === 'string') {
50 throw new Error('target must be a DOM element');
51 }
52
53 if (!this._target.getContext) {
54 throw new Error("no getContext method");
55 }
56
57 this._targetCtx = this._target.getContext('2d');
58
59 // the visible canvas viewport (i.e. what actually gets seen)
60 this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
61
62 // The hidden canvas, where we do the actual rendering
63 this._backbuffer = document.createElement('canvas');
64 this._drawCtx = this._backbuffer.getContext('2d');
65
66 this._damageBounds = { left:0, top:0,
67 right: this._backbuffer.width,
68 bottom: this._backbuffer.height };
69
70 Log.Debug("User Agent: " + navigator.userAgent);
71
72 this.clear();
73
74 // Check canvas features
75 if (!('createImageData' in this._drawCtx)) {
76 throw new Error("Canvas does not support createImageData");
77 }
78
79 Log.Debug("<< Display.constructor");
80 };
81
82 var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false;
83 try {
84 new ImageData(new Uint8ClampedArray(4), 1, 1);
85 SUPPORTS_IMAGEDATA_CONSTRUCTOR = true;
86 } catch (ex) {
87 // ignore failure
88 }
89
90 Display.prototype = {
91 // Public methods
92 viewportChangePos: function (deltaX, deltaY) {
93 var vp = this._viewportLoc;
94 deltaX = Math.floor(deltaX);
95 deltaY = Math.floor(deltaY);
96
97 if (!this._viewport) {
98 deltaX = -vp.w; // clamped later of out of bounds
99 deltaY = -vp.h;
100 }
101
102 var vx2 = vp.x + vp.w - 1;
103 var vy2 = vp.y + vp.h - 1;
104
105 // Position change
106
107 if (deltaX < 0 && vp.x + deltaX < 0) {
108 deltaX = -vp.x;
109 }
110 if (vx2 + deltaX >= this._fb_width) {
111 deltaX -= vx2 + deltaX - this._fb_width + 1;
112 }
113
114 if (vp.y + deltaY < 0) {
115 deltaY = -vp.y;
116 }
117 if (vy2 + deltaY >= this._fb_height) {
118 deltaY -= (vy2 + deltaY - this._fb_height + 1);
119 }
120
121 if (deltaX === 0 && deltaY === 0) {
122 return;
123 }
124 Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
125
126 vp.x += deltaX;
127 vp.y += deltaY;
128
129 this._damage(vp.x, vp.y, vp.w, vp.h);
130
131 this.flip();
132 },
133
134 viewportChangeSize: function(width, height) {
135
136 if (!this._viewport ||
137 typeof(width) === "undefined" ||
138 typeof(height) === "undefined") {
139
140 Log.Debug("Setting viewport to full display region");
141 width = this._fb_width;
142 height = this._fb_height;
143 }
144
145 if (width > this._fb_width) {
146 width = this._fb_width;
147 }
148 if (height > this._fb_height) {
149 height = this._fb_height;
150 }
151
152 var vp = this._viewportLoc;
153 if (vp.w !== width || vp.h !== height) {
154 vp.w = width;
155 vp.h = height;
156
157 var canvas = this._target;
158 canvas.width = width;
159 canvas.height = height;
160
161 // The position might need to be updated if we've grown
162 this.viewportChangePos(0, 0);
163
164 this._damage(vp.x, vp.y, vp.w, vp.h);
165 this.flip();
166
167 // Update the visible size of the target canvas
168 this._rescale(this._scale);
169 }
170 },
171
172 absX: function (x) {
173 return x / this._scale + this._viewportLoc.x;
174 },
175
176 absY: function (y) {
177 return y / this._scale + this._viewportLoc.y;
178 },
179
180 resize: function (width, height) {
181 this._prevDrawStyle = "";
182
183 this._fb_width = width;
184 this._fb_height = height;
185
186 var canvas = this._backbuffer;
187 if (canvas.width !== width || canvas.height !== height) {
188
189 // We have to save the canvas data since changing the size will clear it
190 var saveImg = null;
191 if (canvas.width > 0 && canvas.height > 0) {
192 saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
193 }
194
195 if (canvas.width !== width) {
196 canvas.width = width;
197 }
198 if (canvas.height !== height) {
199 canvas.height = height;
200 }
201
202 if (saveImg) {
203 this._drawCtx.putImageData(saveImg, 0, 0);
204 }
205 }
206
207 // Readjust the viewport as it may be incorrectly sized
208 // and positioned
209 var vp = this._viewportLoc;
210 this.viewportChangeSize(vp.w, vp.h);
211 this.viewportChangePos(0, 0);
212 },
213
214 // Track what parts of the visible canvas that need updating
215 _damage: function(x, y, w, h) {
216 if (x < this._damageBounds.left) {
217 this._damageBounds.left = x;
218 }
219 if (y < this._damageBounds.top) {
220 this._damageBounds.top = y;
221 }
222 if ((x + w) > this._damageBounds.right) {
223 this._damageBounds.right = x + w;
224 }
225 if ((y + h) > this._damageBounds.bottom) {
226 this._damageBounds.bottom = y + h;
227 }
228 },
229
230 // Update the visible canvas with the contents of the
231 // rendering canvas
232 flip: function(from_queue) {
233 if (this._renderQ.length !== 0 && !from_queue) {
234 this._renderQ_push({
235 'type': 'flip'
236 });
237 } else {
238 var x, y, vx, vy, w, h;
239
240 x = this._damageBounds.left;
241 y = this._damageBounds.top;
242 w = this._damageBounds.right - x;
243 h = this._damageBounds.bottom - y;
244
245 vx = x - this._viewportLoc.x;
246 vy = y - this._viewportLoc.y;
247
248 if (vx < 0) {
249 w += vx;
250 x -= vx;
251 vx = 0;
252 }
253 if (vy < 0) {
254 h += vy;
255 y -= vy;
256 vy = 0;
257 }
258
259 if ((vx + w) > this._viewportLoc.w) {
260 w = this._viewportLoc.w - vx;
261 }
262 if ((vy + h) > this._viewportLoc.h) {
263 h = this._viewportLoc.h - vy;
264 }
265
266 if ((w > 0) && (h > 0)) {
267 // FIXME: We may need to disable image smoothing here
268 // as well (see copyImage()), but we haven't
269 // noticed any problem yet.
270 this._targetCtx.drawImage(this._backbuffer,
271 x, y, w, h,
272 vx, vy, w, h);
273 }
274
275 this._damageBounds.left = this._damageBounds.top = 65535;
276 this._damageBounds.right = this._damageBounds.bottom = 0;
277 }
278 },
279
280 clear: function () {
281 if (this._logo) {
282 this.resize(this._logo.width, this._logo.height);
283 this.imageRect(0, 0, this._logo.type, this._logo.data);
284 } else {
285 this.resize(240, 20);
286 this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height);
287 }
288 this.flip();
289 },
290
291 pending: function() {
292 return this._renderQ.length > 0;
293 },
294
295 flush: function() {
296 if (this._renderQ.length === 0) {
297 this._onFlush();
298 } else {
299 this._flushing = true;
300 }
301 },
302
303 fillRect: function (x, y, width, height, color, from_queue) {
304 if (this._renderQ.length !== 0 && !from_queue) {
305 this._renderQ_push({
306 'type': 'fill',
307 'x': x,
308 'y': y,
309 'width': width,
310 'height': height,
311 'color': color
312 });
313 } else {
314 this._setFillColor(color);
315 this._drawCtx.fillRect(x, y, width, height);
316 this._damage(x, y, width, height);
317 }
318 },
319
320 copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) {
321 if (this._renderQ.length !== 0 && !from_queue) {
322 this._renderQ_push({
323 'type': 'copy',
324 'old_x': old_x,
325 'old_y': old_y,
326 'x': new_x,
327 'y': new_y,
328 'width': w,
329 'height': h,
330 });
331 } else {
332 // Due to this bug among others [1] we need to disable the image-smoothing to
333 // avoid getting a blur effect when copying data.
334 //
335 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
336 //
337 // We need to set these every time since all properties are reset
338 // when the the size is changed
339 this._drawCtx.mozImageSmoothingEnabled = false;
340 this._drawCtx.webkitImageSmoothingEnabled = false;
341 this._drawCtx.msImageSmoothingEnabled = false;
342 this._drawCtx.imageSmoothingEnabled = false;
343
344 this._drawCtx.drawImage(this._backbuffer,
345 old_x, old_y, w, h,
346 new_x, new_y, w, h);
347 this._damage(new_x, new_y, w, h);
348 }
349 },
350
351 imageRect: function(x, y, mime, arr) {
352 var img = new Image();
353 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
354 this._renderQ_push({
355 'type': 'img',
356 'img': img,
357 'x': x,
358 'y': y
359 });
360 },
361
362 // start updating a tile
363 startTile: function (x, y, width, height, color) {
364 this._tile_x = x;
365 this._tile_y = y;
366 if (width === 16 && height === 16) {
367 this._tile = this._tile16x16;
368 } else {
369 this._tile = this._drawCtx.createImageData(width, height);
370 }
371
372 var red = color[2];
373 var green = color[1];
374 var blue = color[0];
375
376 var data = this._tile.data;
377 for (var i = 0; i < width * height * 4; i += 4) {
378 data[i] = red;
379 data[i + 1] = green;
380 data[i + 2] = blue;
381 data[i + 3] = 255;
382 }
383 },
384
385 // update sub-rectangle of the current tile
386 subTile: function (x, y, w, h, color) {
387 var red = color[2];
388 var green = color[1];
389 var blue = color[0];
390 var xend = x + w;
391 var yend = y + h;
392
393 var data = this._tile.data;
394 var width = this._tile.width;
395 for (var j = y; j < yend; j++) {
396 for (var i = x; i < xend; i++) {
397 var p = (i + (j * width)) * 4;
398 data[p] = red;
399 data[p + 1] = green;
400 data[p + 2] = blue;
401 data[p + 3] = 255;
402 }
403 }
404 },
405
406 // draw the current tile to the screen
407 finishTile: function () {
408 this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
409 this._damage(this._tile_x, this._tile_y,
410 this._tile.width, this._tile.height);
411 },
412
413 blitImage: function (x, y, width, height, arr, offset, from_queue) {
414 if (this._renderQ.length !== 0 && !from_queue) {
415 // NB(directxman12): it's technically more performant here to use preallocated arrays,
416 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
417 // this probably isn't getting called *nearly* as much
418 var new_arr = new Uint8Array(width * height * 4);
419 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
420 this._renderQ_push({
421 'type': 'blit',
422 'data': new_arr,
423 'x': x,
424 'y': y,
425 'width': width,
426 'height': height,
427 });
428 } else {
429 this._bgrxImageData(x, y, width, height, arr, offset);
430 }
431 },
432
433 blitRgbImage: function (x, y , width, height, arr, offset, from_queue) {
434 if (this._renderQ.length !== 0 && !from_queue) {
435 // NB(directxman12): it's technically more performant here to use preallocated arrays,
436 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
437 // this probably isn't getting called *nearly* as much
438 var new_arr = new Uint8Array(width * height * 3);
439 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
440 this._renderQ_push({
441 'type': 'blitRgb',
442 'data': new_arr,
443 'x': x,
444 'y': y,
445 'width': width,
446 'height': height,
447 });
448 } else {
449 this._rgbImageData(x, y, width, height, arr, offset);
450 }
451 },
452
453 blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) {
454 if (this._renderQ.length !== 0 && !from_queue) {
455 // NB(directxman12): it's technically more performant here to use preallocated arrays,
456 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
457 // this probably isn't getting called *nearly* as much
458 var new_arr = new Uint8Array(width * height * 4);
459 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
460 this._renderQ_push({
461 'type': 'blitRgbx',
462 'data': new_arr,
463 'x': x,
464 'y': y,
465 'width': width,
466 'height': height,
467 });
468 } else {
469 this._rgbxImageData(x, y, width, height, arr, offset);
470 }
471 },
472
473 drawImage: function (img, x, y) {
474 this._drawCtx.drawImage(img, x, y);
475 this._damage(x, y, img.width, img.height);
476 },
477
478 changeCursor: function (pixels, mask, hotx, hoty, w, h) {
479 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
480 },
481
482 defaultCursor: function () {
483 this._target.style.cursor = "default";
484 },
485
486 disableLocalCursor: function () {
487 this._target.style.cursor = "none";
488 },
489
490 clippingDisplay: function () {
491 var vp = this._viewportLoc;
492 return this._fb_width > vp.w || this._fb_height > vp.h;
493 },
494
495 // Overridden getters/setters
496 set_scale: function (scale) {
497 this._rescale(scale);
498 },
499
500 set_viewport: function (viewport) {
501 this._viewport = viewport;
502 // May need to readjust the viewport dimensions
503 var vp = this._viewportLoc;
504 this.viewportChangeSize(vp.w, vp.h);
505 this.viewportChangePos(0, 0);
506 },
507
508 get_width: function () {
509 return this._fb_width;
510 },
511 get_height: function () {
512 return this._fb_height;
513 },
514
515 autoscale: function (containerWidth, containerHeight, downscaleOnly) {
516 var vp = this._viewportLoc;
517 var targetAspectRatio = containerWidth / containerHeight;
518 var fbAspectRatio = vp.w / vp.h;
519
520 var scaleRatio;
521 if (fbAspectRatio >= targetAspectRatio) {
522 scaleRatio = containerWidth / vp.w;
523 } else {
524 scaleRatio = containerHeight / vp.h;
525 }
526
527 if (scaleRatio > 1.0 && downscaleOnly) {
528 scaleRatio = 1.0;
529 }
530
531 this._rescale(scaleRatio);
532 },
533
534 // Private Methods
535 _rescale: function (factor) {
536 this._scale = factor;
537 var vp = this._viewportLoc;
538
539 // NB(directxman12): If you set the width directly, or set the
540 // style width to a number, the canvas is cleared.
541 // However, if you set the style width to a string
542 // ('NNNpx'), the canvas is scaled without clearing.
543 var width = Math.round(factor * vp.w) + 'px';
544 var height = Math.round(factor * vp.h) + 'px';
545
546 if ((this._target.style.width !== width) ||
547 (this._target.style.height !== height)) {
548 this._target.style.width = width;
549 this._target.style.height = height;
550 }
551 },
552
553 _setFillColor: function (color) {
554 var newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')';
555 if (newStyle !== this._prevDrawStyle) {
556 this._drawCtx.fillStyle = newStyle;
557 this._prevDrawStyle = newStyle;
558 }
559 },
560
561 _rgbImageData: function (x, y, width, height, arr, offset) {
562 var img = this._drawCtx.createImageData(width, height);
563 var data = img.data;
564 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
565 data[i] = arr[j];
566 data[i + 1] = arr[j + 1];
567 data[i + 2] = arr[j + 2];
568 data[i + 3] = 255; // Alpha
569 }
570 this._drawCtx.putImageData(img, x, y);
571 this._damage(x, y, img.width, img.height);
572 },
573
574 _bgrxImageData: function (x, y, width, height, arr, offset) {
575 var img = this._drawCtx.createImageData(width, height);
576 var data = img.data;
577 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
578 data[i] = arr[j + 2];
579 data[i + 1] = arr[j + 1];
580 data[i + 2] = arr[j];
581 data[i + 3] = 255; // Alpha
582 }
583 this._drawCtx.putImageData(img, x, y);
584 this._damage(x, y, img.width, img.height);
585 },
586
587 _rgbxImageData: function (x, y, width, height, arr, offset) {
588 // NB(directxman12): arr must be an Type Array view
589 var img;
590 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
591 img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
592 } else {
593 img = this._drawCtx.createImageData(width, height);
594 img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
595 }
596 this._drawCtx.putImageData(img, x, y);
597 this._damage(x, y, img.width, img.height);
598 },
599
600 _renderQ_push: function (action) {
601 this._renderQ.push(action);
602 if (this._renderQ.length === 1) {
603 // If this can be rendered immediately it will be, otherwise
604 // the scanner will wait for the relevant event
605 this._scan_renderQ();
606 }
607 },
608
609 _resume_renderQ: function() {
610 // "this" is the object that is ready, not the
611 // display object
612 this.removeEventListener('load', this._noVNC_display._resume_renderQ);
613 this._noVNC_display._scan_renderQ();
614 },
615
616 _scan_renderQ: function () {
617 var ready = true;
618 while (ready && this._renderQ.length > 0) {
619 var a = this._renderQ[0];
620 switch (a.type) {
621 case 'flip':
622 this.flip(true);
623 break;
624 case 'copy':
625 this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true);
626 break;
627 case 'fill':
628 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
629 break;
630 case 'blit':
631 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
632 break;
633 case 'blitRgb':
634 this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
635 break;
636 case 'blitRgbx':
637 this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
638 break;
639 case 'img':
640 if (a.img.complete) {
641 this.drawImage(a.img, a.x, a.y);
642 } else {
643 a.img._noVNC_display = this;
644 a.img.addEventListener('load', this._resume_renderQ);
645 // We need to wait for this image to 'load'
646 // to keep things in-order
647 ready = false;
648 }
649 break;
650 }
651
652 if (ready) {
653 this._renderQ.shift();
654 }
655 }
656
657 if (this._renderQ.length === 0 && this._flushing) {
658 this._flushing = false;
659 this._onFlush();
660 }
661 },
662 };
663
664 make_properties(Display, [
665 ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
666 ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "type": mime-type, "data": data}
667 ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
668 ['viewport', 'rw', 'bool'], // Use viewport clipping
669 ['width', 'ro', 'int'], // Display area width
670 ['height', 'ro', 'int'], // Display area height
671
672 ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
673 ]);
674
675 // Class Methods
676 Display.changeCursor = function (target, pixels, mask, hotx, hoty, w, h) {
677 if ((w === 0) || (h === 0)) {
678 target.style.cursor = 'none';
679 return;
680 }
681
682 var cur = []
683 var y, x;
684 for (y = 0; y < h; y++) {
685 for (x = 0; x < w; x++) {
686 var idx = y * Math.ceil(w / 8) + Math.floor(x / 8);
687 var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
688 idx = ((w * y) + x) * 4;
689 cur.push(pixels[idx + 2]); // red
690 cur.push(pixels[idx + 1]); // green
691 cur.push(pixels[idx]); // blue
692 cur.push(alpha); // alpha
693 }
694 }
695
696 var canvas = document.createElement('canvas');
697 var ctx = canvas.getContext('2d');
698
699 canvas.width = w;
700 canvas.height = h;
701
702 var img;
703 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
704 img = new ImageData(new Uint8ClampedArray(cur), w, h);
705 } else {
706 img = ctx.createImageData(w, h);
707 img.data.set(new Uint8ClampedArray(cur));
708 }
709 ctx.clearRect(0, 0, w, h);
710 ctx.putImageData(img, 0, 0);
711
712 var url = canvas.toDataURL();
713 target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
714 };