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