]> git.proxmox.com Git - mirror_novnc.git/blame - core/display.js
Fix typos in imports and i18n
[mirror_novnc.git] / core / display.js
CommitLineData
c4164bda
JM
1/*
2 * noVNC: HTML5 VNC client
d58f8b51 3 * Copyright (C) 2012 Joel Martin
fdedbafb 4 * Copyright (C) 2015 Samuel Mannehed for Cendio AB
1d728ace 5 * Licensed under MPL 2.0 (see LICENSE.txt)
c4164bda
JM
6 *
7 * See README.md for usage and integration instructions.
8 */
c4164bda 9
1e13775b 10/*jslint browser: true, white: false */
d3796c14 11/*global Util, Base64, changeCursor */
c4164bda 12
ae510306
SR
13/* [module]
14 * import Util from "./util";
15 * import Base64 from "./base64";
16 */
17
18/* [module] export default */ function Display(defaults) {
19 this._drawCtx = null;
20 this._c_forceCanvas = false;
21
22 this._renderQ = []; // queue drawing actions for in-oder rendering
d9ca5e5b 23 this._flushing = false;
ae510306
SR
24
25 // the full frame buffer (logical canvas) size
26 this._fb_width = 0;
27 this._fb_height = 0;
28
ae510306
SR
29 this._prevDrawStyle = "";
30 this._tile = null;
31 this._tile16x16 = null;
32 this._tile_x = 0;
33 this._tile_y = 0;
34
35 Util.set_defaults(this, defaults, {
36 'true_color': true,
37 'colourMap': [],
38 'scale': 1.0,
39 'viewport': false,
d9ca5e5b
PO
40 'render_mode': '',
41 "onFlush": function () {},
ae510306
SR
42 });
43
44 Util.Debug(">> Display.constructor");
45
2ba767a7 46 // The visible canvas
ae510306
SR
47 if (!this._target) {
48 throw new Error("Target must be set");
49 }
50
51 if (typeof this._target === 'string') {
52 throw new Error('target must be a DOM element');
53 }
54
55 if (!this._target.getContext) {
56 throw new Error("no getContext method");
57 }
58
2ba767a7
PO
59 this._targetCtx = this._target.getContext('2d');
60
adf345fd
PO
61 // the visible canvas viewport (i.e. what actually gets seen)
62 this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
63
2ba767a7
PO
64 // The hidden canvas, where we do the actual rendering
65 this._backbuffer = document.createElement('canvas');
66 this._drawCtx = this._backbuffer.getContext('2d');
ae510306 67
84cd0e71
PO
68 this._damageBounds = { left:0, top:0,
69 right: this._backbuffer.width,
70 bottom: this._backbuffer.height };
71
ae510306
SR
72 Util.Debug("User Agent: " + navigator.userAgent);
73 if (Util.Engine.gecko) { Util.Debug("Browser: gecko " + Util.Engine.gecko); }
74 if (Util.Engine.webkit) { Util.Debug("Browser: webkit " + Util.Engine.webkit); }
75 if (Util.Engine.trident) { Util.Debug("Browser: trident " + Util.Engine.trident); }
76 if (Util.Engine.presto) { Util.Debug("Browser: presto " + Util.Engine.presto); }
77
78 this.clear();
79
80 // Check canvas features
81 if ('createImageData' in this._drawCtx) {
82 this._render_mode = 'canvas rendering';
83 } else {
84 throw new Error("Canvas does not support createImageData");
85 }
86
87 if (this._prefer_js === null) {
88 Util.Info("Prefering javascript operations");
89 this._prefer_js = true;
90 }
91
92 // Determine browser support for setting the cursor via data URI scheme
93 if (this._cursor_uri || this._cursor_uri === null ||
94 this._cursor_uri === undefined) {
95 this._cursor_uri = Util.browserSupportsCursorURIs();
96 }
97
98 Util.Debug("<< Display.constructor");
99};
ff36b127 100
1e13775b
SR
101(function () {
102 "use strict";
e2e7c224 103
d1800d09
SR
104 var SUPPORTS_IMAGEDATA_CONSTRUCTOR = false;
105 try {
6521c6ac 106 new ImageData(new Uint8ClampedArray(4), 1, 1);
d1800d09
SR
107 SUPPORTS_IMAGEDATA_CONSTRUCTOR = true;
108 } catch (ex) {
109 // ignore failure
110 }
111
490d471c 112
1e13775b
SR
113 Display.prototype = {
114 // Public methods
636be753 115 viewportChangePos: function (deltaX, deltaY) {
1e13775b 116 var vp = this._viewportLoc;
a8255821 117 deltaX = Math.floor(deltaX);
118 deltaY = Math.floor(deltaY);
1e13775b
SR
119
120 if (!this._viewport) {
1e13775b
SR
121 deltaX = -vp.w; // clamped later of out of bounds
122 deltaY = -vp.h;
8db09746 123 }
54e7cbdf 124
1e13775b
SR
125 var vx2 = vp.x + vp.w - 1;
126 var vy2 = vp.y + vp.h - 1;
54e7cbdf 127
1e13775b 128 // Position change
54e7cbdf 129
1e13775b
SR
130 if (deltaX < 0 && vp.x + deltaX < 0) {
131 deltaX = -vp.x;
132 }
133 if (vx2 + deltaX >= this._fb_width) {
134 deltaX -= vx2 + deltaX - this._fb_width + 1;
135 }
54e7cbdf 136
1e13775b
SR
137 if (vp.y + deltaY < 0) {
138 deltaY = -vp.y;
139 }
140 if (vy2 + deltaY >= this._fb_height) {
141 deltaY -= (vy2 + deltaY - this._fb_height + 1);
142 }
54e7cbdf 143
1e13775b
SR
144 if (deltaX === 0 && deltaY === 0) {
145 return;
146 }
147 Util.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
148
149 vp.x += deltaX;
1e13775b 150 vp.y += deltaY;
a8255821 151
84cd0e71
PO
152 this._damage(vp.x, vp.y, vp.w, vp.h);
153
2ba767a7 154 this.flip();
1e13775b
SR
155 },
156
636be753 157 viewportChangeSize: function(width, height) {
158
adf345fd
PO
159 if (!this._viewport ||
160 typeof(width) === "undefined" ||
161 typeof(height) === "undefined") {
636be753 162
163 Util.Debug("Setting viewport to full display region");
164 width = this._fb_width;
165 height = this._fb_height;
166 }
167
adf345fd
PO
168 if (width > this._fb_width) {
169 width = this._fb_width;
170 }
171 if (height > this._fb_height) {
172 height = this._fb_height;
173 }
174
636be753 175 var vp = this._viewportLoc;
176 if (vp.w !== width || vp.h !== height) {
fdedbafb 177 vp.w = width;
178 vp.h = height;
636be753 179
636be753 180 var canvas = this._target;
adf345fd
PO
181 canvas.width = width;
182 canvas.height = height;
183
184 // The position might need to be updated if we've grown
185 this.viewportChangePos(0, 0);
186
187 this._damage(vp.x, vp.y, vp.w, vp.h);
188 this.flip();
189
190 // Update the visible size of the target canvas
191 this._rescale(this._scale);
636be753 192 }
193 },
194
1e13775b 195 absX: function (x) {
280676c7 196 return x / this._scale + this._viewportLoc.x;
1e13775b
SR
197 },
198
199 absY: function (y) {
280676c7 200 return y / this._scale + this._viewportLoc.y;
1e13775b
SR
201 },
202
203 resize: function (width, height) {
204 this._prevDrawStyle = "";
205
206 this._fb_width = width;
207 this._fb_height = height;
208
2ba767a7
PO
209 var canvas = this._backbuffer;
210 if (canvas.width !== width || canvas.height !== height) {
211
212 // We have to save the canvas data since changing the size will clear it
213 var saveImg = null;
214 if (canvas.width > 0 && canvas.height > 0) {
215 saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
216 }
217
218 if (canvas.width !== width) {
219 canvas.width = width;
220 }
221 if (canvas.height !== height) {
222 canvas.height = height;
223 }
224
225 if (saveImg) {
226 this._drawCtx.putImageData(saveImg, 0, 0);
227 }
228 }
229
adf345fd
PO
230 // Readjust the viewport as it may be incorrectly sized
231 // and positioned
232 var vp = this._viewportLoc;
233 this.viewportChangeSize(vp.w, vp.h);
234 this.viewportChangePos(0, 0);
1e13775b
SR
235 },
236
84cd0e71
PO
237 // Track what parts of the visible canvas that need updating
238 _damage: function(x, y, w, h) {
239 if (x < this._damageBounds.left) {
240 this._damageBounds.left = x;
241 }
242 if (y < this._damageBounds.top) {
243 this._damageBounds.top = y;
244 }
245 if ((x + w) > this._damageBounds.right) {
246 this._damageBounds.right = x + w;
247 }
248 if ((y + h) > this._damageBounds.bottom) {
249 this._damageBounds.bottom = y + h;
250 }
251 },
252
2ba767a7
PO
253 // Update the visible canvas with the contents of the
254 // rendering canvas
255 flip: function(from_queue) {
256 if (this._renderQ.length !== 0 && !from_queue) {
257 this._renderQ_push({
258 'type': 'flip'
259 });
260 } else {
84cd0e71
PO
261 var x, y, vx, vy, w, h;
262
263 x = this._damageBounds.left;
264 y = this._damageBounds.top;
265 w = this._damageBounds.right - x;
266 h = this._damageBounds.bottom - y;
267
268 vx = x - this._viewportLoc.x;
269 vy = y - this._viewportLoc.y;
270
271 if (vx < 0) {
272 w += vx;
273 x -= vx;
274 vx = 0;
275 }
276 if (vy < 0) {
277 h += vy;
278 y -= vy;
279 vy = 0;
280 }
281
282 if ((vx + w) > this._viewportLoc.w) {
283 w = this._viewportLoc.w - vx;
284 }
285 if ((vy + h) > this._viewportLoc.h) {
286 h = this._viewportLoc.h - vy;
287 }
288
289 if ((w > 0) && (h > 0)) {
290 // FIXME: We may need to disable image smoothing here
291 // as well (see copyImage()), but we haven't
292 // noticed any problem yet.
293 this._targetCtx.drawImage(this._backbuffer,
294 x, y, w, h,
295 vx, vy, w, h);
296 }
297
298 this._damageBounds.left = this._damageBounds.top = 65535;
299 this._damageBounds.right = this._damageBounds.bottom = 0;
2ba767a7
PO
300 }
301 },
302
1e13775b
SR
303 clear: function () {
304 if (this._logo) {
305 this.resize(this._logo.width, this._logo.height);
74e39051 306 this.imageRect(0, 0, this._logo.type, this._logo.data);
1e13775b 307 } else {
795fca23 308 this.resize(240, 20);
2ba767a7 309 this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height);
1e13775b 310 }
2ba767a7 311 this.flip();
1e13775b
SR
312 },
313
d9ca5e5b
PO
314 pending: function() {
315 return this._renderQ.length > 0;
316 },
317
318 flush: function() {
319 if (this._renderQ.length === 0) {
320 this._onFlush();
321 } else {
322 this._flushing = true;
323 }
324 },
325
f00193e0
SR
326 fillRect: function (x, y, width, height, color, from_queue) {
327 if (this._renderQ.length !== 0 && !from_queue) {
1578fa68 328 this._renderQ_push({
f00193e0
SR
329 'type': 'fill',
330 'x': x,
331 'y': y,
332 'width': width,
333 'height': height,
334 'color': color
335 });
336 } else {
337 this._setFillColor(color);
2ba767a7 338 this._drawCtx.fillRect(x, y, width, height);
84cd0e71 339 this._damage(x, y, width, height);
f00193e0
SR
340 }
341 },
342
343 copyImage: function (old_x, old_y, new_x, new_y, w, h, from_queue) {
344 if (this._renderQ.length !== 0 && !from_queue) {
1578fa68 345 this._renderQ_push({
f00193e0
SR
346 'type': 'copy',
347 'old_x': old_x,
348 'old_y': old_y,
349 'x': new_x,
350 'y': new_y,
351 'width': w,
352 'height': h,
353 });
354 } else {
2ba767a7
PO
355 // Due to this bug among others [1] we need to disable the image-smoothing to
356 // avoid getting a blur effect when copying data.
357 //
358 // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
359 //
360 // We need to set these every time since all properties are reset
361 // when the the size is changed
362 this._drawCtx.mozImageSmoothingEnabled = false;
363 this._drawCtx.webkitImageSmoothingEnabled = false;
364 this._drawCtx.msImageSmoothingEnabled = false;
365 this._drawCtx.imageSmoothingEnabled = false;
1e13775b 366
2ba767a7
PO
367 this._drawCtx.drawImage(this._backbuffer,
368 old_x, old_y, w, h,
369 new_x, new_y, w, h);
84cd0e71 370 this._damage(new_x, new_y, w, h);
f00193e0 371 }
1e13775b
SR
372 },
373
1578fa68
PO
374 imageRect: function(x, y, mime, arr) {
375 var img = new Image();
376 img.src = "data: " + mime + ";base64," + Base64.encode(arr);
377 this._renderQ_push({
378 'type': 'img',
379 'img': img,
380 'x': x,
381 'y': y
382 });
383 },
384
1e13775b
SR
385 // start updating a tile
386 startTile: function (x, y, width, height, color) {
387 this._tile_x = x;
388 this._tile_y = y;
389 if (width === 16 && height === 16) {
390 this._tile = this._tile16x16;
391 } else {
392 this._tile = this._drawCtx.createImageData(width, height);
393 }
394
395 if (this._prefer_js) {
396 var bgr;
397 if (this._true_color) {
398 bgr = color;
34d8b844 399 } else {
1e13775b 400 bgr = this._colourMap[color[0]];
34d8b844 401 }
1e13775b
SR
402 var red = bgr[2];
403 var green = bgr[1];
404 var blue = bgr[0];
405
406 var data = this._tile.data;
407 for (var i = 0; i < width * height * 4; i += 4) {
408 data[i] = red;
409 data[i + 1] = green;
410 data[i + 2] = blue;
411 data[i + 3] = 255;
412 }
413 } else {
f00193e0 414 this.fillRect(x, y, width, height, color, true);
1e13775b
SR
415 }
416 },
417
418 // update sub-rectangle of the current tile
419 subTile: function (x, y, w, h, color) {
420 if (this._prefer_js) {
421 var bgr;
422 if (this._true_color) {
423 bgr = color;
424 } else {
425 bgr = this._colourMap[color[0]];
426 }
427 var red = bgr[2];
428 var green = bgr[1];
429 var blue = bgr[0];
430 var xend = x + w;
431 var yend = y + h;
432
433 var data = this._tile.data;
434 var width = this._tile.width;
435 for (var j = y; j < yend; j++) {
436 for (var i = x; i < xend; i++) {
437 var p = (i + (j * width)) * 4;
438 data[p] = red;
439 data[p + 1] = green;
440 data[p + 2] = blue;
441 data[p + 3] = 255;
442 }
443 }
444 } else {
f00193e0 445 this.fillRect(this._tile_x + x, this._tile_y + y, w, h, color, true);
1e13775b
SR
446 }
447 },
34d8b844 448
1e13775b
SR
449 // draw the current tile to the screen
450 finishTile: function () {
451 if (this._prefer_js) {
2ba767a7 452 this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y);
84cd0e71
PO
453 this._damage(this._tile_x, this._tile_y,
454 this._tile.width, this._tile.height);
1e13775b
SR
455 }
456 // else: No-op -- already done by setSubTile
457 },
bc28395a 458
f00193e0
SR
459 blitImage: function (x, y, width, height, arr, offset, from_queue) {
460 if (this._renderQ.length !== 0 && !from_queue) {
7bc383e8
SR
461 // NB(directxman12): it's technically more performant here to use preallocated arrays,
462 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
463 // this probably isn't getting called *nearly* as much
464 var new_arr = new Uint8Array(width * height * 4);
465 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
1578fa68 466 this._renderQ_push({
f00193e0 467 'type': 'blit',
7bc383e8 468 'data': new_arr,
f00193e0
SR
469 'x': x,
470 'y': y,
471 'width': width,
472 'height': height,
473 });
474 } else if (this._true_color) {
2ba767a7 475 this._bgrxImageData(x, y, width, height, arr, offset);
1e13775b 476 } else {
2ba767a7 477 this._cmapImageData(x, y, width, height, arr, offset);
1e13775b
SR
478 }
479 },
2c2b492c 480
f00193e0
SR
481 blitRgbImage: function (x, y , width, height, arr, offset, from_queue) {
482 if (this._renderQ.length !== 0 && !from_queue) {
7bc383e8
SR
483 // NB(directxman12): it's technically more performant here to use preallocated arrays,
484 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
485 // this probably isn't getting called *nearly* as much
6024677f 486 var new_arr = new Uint8Array(width * height * 3);
7bc383e8 487 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
1578fa68 488 this._renderQ_push({
f00193e0 489 'type': 'blitRgb',
7bc383e8 490 'data': new_arr,
f00193e0
SR
491 'x': x,
492 'y': y,
493 'width': width,
494 'height': height,
495 });
496 } else if (this._true_color) {
2ba767a7 497 this._rgbImageData(x, y, width, height, arr, offset);
1e13775b
SR
498 } else {
499 // probably wrong?
2ba767a7 500 this._cmapImageData(x, y, width, height, arr, offset);
1e13775b
SR
501 }
502 },
503
f00193e0
SR
504 blitRgbxImage: function (x, y, width, height, arr, offset, from_queue) {
505 if (this._renderQ.length !== 0 && !from_queue) {
7bc383e8 506 // NB(directxman12): it's technically more performant here to use preallocated arrays,
f00193e0
SR
507 // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
508 // this probably isn't getting called *nearly* as much
509 var new_arr = new Uint8Array(width * height * 4);
510 new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length));
1578fa68 511 this._renderQ_push({
f00193e0
SR
512 'type': 'blitRgbx',
513 'data': new_arr,
514 'x': x,
515 'y': y,
516 'width': width,
517 'height': height,
518 });
519 } else {
2ba767a7 520 this._rgbxImageData(x, y, width, height, arr, offset);
f00193e0 521 }
d1800d09
SR
522 },
523
1e13775b 524 drawImage: function (img, x, y) {
2ba767a7 525 this._drawCtx.drawImage(img, x, y);
84cd0e71 526 this._damage(x, y, img.width, img.height);
1e13775b
SR
527 },
528
1e13775b
SR
529 changeCursor: function (pixels, mask, hotx, hoty, w, h) {
530 if (this._cursor_uri === false) {
531 Util.Warn("changeCursor called but no cursor data URI support");
532 return;
533 }
d3796c14 534
1e13775b
SR
535 if (this._true_color) {
536 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h);
537 } else {
538 Display.changeCursor(this._target, pixels, mask, hotx, hoty, w, h, this._colourMap);
539 }
540 },
541
542 defaultCursor: function () {
543 this._target.style.cursor = "default";
544 },
545
b804b3e4 546 disableLocalCursor: function () {
547 this._target.style.cursor = "none";
548 },
549
fdedbafb 550 clippingDisplay: function () {
551 var vp = this._viewportLoc;
3f781f2a 552 return this._fb_width > vp.w || this._fb_height > vp.h;
636be753 553 },
554
1e13775b 555 // Overridden getters/setters
1e13775b
SR
556 set_scale: function (scale) {
557 this._rescale(scale);
558 },
559
adf345fd
PO
560 set_viewport: function (viewport) {
561 this._viewport = viewport;
562 // May need to readjust the viewport dimensions
563 var vp = this._viewportLoc;
564 this.viewportChangeSize(vp.w, vp.h);
565 this.viewportChangePos(0, 0);
566 },
567
1e13775b
SR
568 get_width: function () {
569 return this._fb_width;
570 },
1e13775b
SR
571 get_height: function () {
572 return this._fb_height;
573 },
574
72747869 575 autoscale: function (containerWidth, containerHeight, downscaleOnly) {
a6e52f9a 576 var vp = this._viewportLoc;
72747869 577 var targetAspectRatio = containerWidth / containerHeight;
a6e52f9a 578 var fbAspectRatio = vp.w / vp.h;
9a23006e 579
72747869
SR
580 var scaleRatio;
581 if (fbAspectRatio >= targetAspectRatio) {
a6e52f9a 582 scaleRatio = containerWidth / vp.w;
72747869 583 } else {
a6e52f9a 584 scaleRatio = containerHeight / vp.h;
1e13775b 585 }
9a23006e 586
72747869 587 if (scaleRatio > 1.0 && downscaleOnly) {
72747869 588 scaleRatio = 1.0;
1e13775b 589 }
9a23006e 590
a6e52f9a 591 this._rescale(scaleRatio);
72747869 592 },
1e13775b 593
72747869
SR
594 // Private Methods
595 _rescale: function (factor) {
1e13775b 596 this._scale = factor;
3f781f2a 597 var vp = this._viewportLoc;
a6e52f9a
PO
598
599 // NB(directxman12): If you set the width directly, or set the
600 // style width to a number, the canvas is cleared.
601 // However, if you set the style width to a string
602 // ('NNNpx'), the canvas is scaled without clearing.
2e6a58fb
PO
603 var width = Math.round(factor * vp.w) + 'px';
604 var height = Math.round(factor * vp.h) + 'px';
605
606 if ((this._target.style.width !== width) ||
607 (this._target.style.height !== height)) {
608 this._target.style.width = width;
609 this._target.style.height = height;
610 }
1e13775b 611 },
67b4e987 612
1e13775b
SR
613 _setFillColor: function (color) {
614 var bgr;
615 if (this._true_color) {
616 bgr = color;
9a23006e 617 } else {
a369a80c 618 bgr = this._colourMap[color];
1e13775b
SR
619 }
620
621 var newStyle = 'rgb(' + bgr[2] + ',' + bgr[1] + ',' + bgr[0] + ')';
622 if (newStyle !== this._prevDrawStyle) {
623 this._drawCtx.fillStyle = newStyle;
624 this._prevDrawStyle = newStyle;
625 }
626 },
627
2ba767a7 628 _rgbImageData: function (x, y, width, height, arr, offset) {
1e13775b
SR
629 var img = this._drawCtx.createImageData(width, height);
630 var data = img.data;
631 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 3) {
632 data[i] = arr[j];
633 data[i + 1] = arr[j + 1];
634 data[i + 2] = arr[j + 2];
635 data[i + 3] = 255; // Alpha
636 }
2ba767a7 637 this._drawCtx.putImageData(img, x, y);
84cd0e71 638 this._damage(x, y, img.width, img.height);
1e13775b
SR
639 },
640
2ba767a7 641 _bgrxImageData: function (x, y, width, height, arr, offset) {
1e13775b
SR
642 var img = this._drawCtx.createImageData(width, height);
643 var data = img.data;
644 for (var i = 0, j = offset; i < width * height * 4; i += 4, j += 4) {
645 data[i] = arr[j + 2];
646 data[i + 1] = arr[j + 1];
647 data[i + 2] = arr[j];
648 data[i + 3] = 255; // Alpha
649 }
2ba767a7 650 this._drawCtx.putImageData(img, x, y);
84cd0e71 651 this._damage(x, y, img.width, img.height);
1e13775b
SR
652 },
653
2ba767a7 654 _rgbxImageData: function (x, y, width, height, arr, offset) {
d1800d09 655 // NB(directxman12): arr must be an Type Array view
d1800d09
SR
656 var img;
657 if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) {
f00193e0 658 img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height);
d1800d09
SR
659 } else {
660 img = this._drawCtx.createImageData(width, height);
f00193e0 661 img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4));
d1800d09 662 }
2ba767a7 663 this._drawCtx.putImageData(img, x, y);
84cd0e71 664 this._damage(x, y, img.width, img.height);
d1800d09
SR
665 },
666
2ba767a7 667 _cmapImageData: function (x, y, width, height, arr, offset) {
1e13775b
SR
668 var img = this._drawCtx.createImageData(width, height);
669 var data = img.data;
670 var cmap = this._colourMap;
671 for (var i = 0, j = offset; i < width * height * 4; i += 4, j++) {
672 var bgr = cmap[arr[j]];
673 data[i] = bgr[2];
674 data[i + 1] = bgr[1];
675 data[i + 2] = bgr[0];
676 data[i + 3] = 255; // Alpha
677 }
2ba767a7 678 this._drawCtx.putImageData(img, x, y);
84cd0e71 679 this._damage(x, y, img.width, img.height);
1e13775b
SR
680 },
681
1578fa68
PO
682 _renderQ_push: function (action) {
683 this._renderQ.push(action);
684 if (this._renderQ.length === 1) {
685 // If this can be rendered immediately it will be, otherwise
bb6965f2 686 // the scanner will wait for the relevant event
1578fa68
PO
687 this._scan_renderQ();
688 }
689 },
690
bb6965f2
PO
691 _resume_renderQ: function() {
692 // "this" is the object that is ready, not the
693 // display object
694 this.removeEventListener('load', this._noVNC_display._resume_renderQ);
695 this._noVNC_display._scan_renderQ();
696 },
697
1e13775b
SR
698 _scan_renderQ: function () {
699 var ready = true;
700 while (ready && this._renderQ.length > 0) {
701 var a = this._renderQ[0];
702 switch (a.type) {
2ba767a7
PO
703 case 'flip':
704 this.flip(true);
705 break;
1e13775b 706 case 'copy':
f00193e0 707 this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true);
1e13775b
SR
708 break;
709 case 'fill':
f00193e0 710 this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
1e13775b
SR
711 break;
712 case 'blit':
f00193e0 713 this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
1e13775b
SR
714 break;
715 case 'blitRgb':
f00193e0 716 this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true);
1e13775b 717 break;
d1800d09 718 case 'blitRgbx':
f00193e0 719 this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true);
1e13775b
SR
720 break;
721 case 'img':
722 if (a.img.complete) {
723 this.drawImage(a.img, a.x, a.y);
724 } else {
bb6965f2
PO
725 a.img._noVNC_display = this;
726 a.img.addEventListener('load', this._resume_renderQ);
1e13775b
SR
727 // We need to wait for this image to 'load'
728 // to keep things in-order
729 ready = false;
730 }
731 break;
732 }
733
734 if (ready) {
735 this._renderQ.shift();
736 }
737 }
d9ca5e5b
PO
738
739 if (this._renderQ.length === 0 && this._flushing) {
740 this._flushing = false;
741 this._onFlush();
742 }
1e13775b
SR
743 },
744 };
745
746 Util.make_properties(Display, [
747 ['target', 'wo', 'dom'], // Canvas element for rendering
748 ['context', 'ro', 'raw'], // Canvas 2D context for rendering (read-only)
74e39051 749 ['logo', 'rw', 'raw'], // Logo to display when cleared: {"width": w, "height": h, "type": mime-type, "data": data}
1e13775b
SR
750 ['true_color', 'rw', 'bool'], // Use true-color pixel data
751 ['colourMap', 'rw', 'arr'], // Colour map array (when not true-color)
752 ['scale', 'rw', 'float'], // Display area scale factor 0.0 - 1.0
fdedbafb 753 ['viewport', 'rw', 'bool'], // Use viewport clipping
e549ae07
PO
754 ['width', 'ro', 'int'], // Display area width
755 ['height', 'ro', 'int'], // Display area height
1e13775b
SR
756
757 ['render_mode', 'ro', 'str'], // Canvas rendering mode (read-only)
758
759 ['prefer_js', 'rw', 'str'], // Prefer Javascript over canvas methods
d9ca5e5b
PO
760 ['cursor_uri', 'rw', 'raw'], // Can we render cursor using data URI
761
762 ['onFlush', 'rw', 'func'], // onFlush(): A flush request has finished
1e13775b
SR
763 ]);
764
765 // Class Methods
766 Display.changeCursor = function (target, pixels, mask, hotx, hoty, w0, h0, cmap) {
767 var w = w0;
768 var h = h0;
769 if (h < w) {
770 h = w; // increase h to make it square
771 } else {
772 w = h; // increase w to make it square
773 }
774
775 var cur = [];
776
777 // Push multi-byte little-endian values
778 cur.push16le = function (num) {
779 this.push(num & 0xFF, (num >> 8) & 0xFF);
780 };
781 cur.push32le = function (num) {
782 this.push(num & 0xFF,
783 (num >> 8) & 0xFF,
784 (num >> 16) & 0xFF,
785 (num >> 24) & 0xFF);
786 };
787
788 var IHDRsz = 40;
789 var RGBsz = w * h * 4;
790 var XORsz = Math.ceil((w * h) / 8.0);
791 var ANDsz = Math.ceil((w * h) / 8.0);
792
793 cur.push16le(0); // 0: Reserved
794 cur.push16le(2); // 2: .CUR type
795 cur.push16le(1); // 4: Number of images, 1 for non-animated ico
796
797 // Cursor #1 header (ICONDIRENTRY)
798 cur.push(w); // 6: width
799 cur.push(h); // 7: height
800 cur.push(0); // 8: colors, 0 -> true-color
801 cur.push(0); // 9: reserved
802 cur.push16le(hotx); // 10: hotspot x coordinate
803 cur.push16le(hoty); // 12: hotspot y coordinate
804 cur.push32le(IHDRsz + RGBsz + XORsz + ANDsz);
805 // 14: cursor data byte size
806 cur.push32le(22); // 18: offset of cursor data in the file
807
808 // Cursor #1 InfoHeader (ICONIMAGE/BITMAPINFO)
809 cur.push32le(IHDRsz); // 22: InfoHeader size
810 cur.push32le(w); // 26: Cursor width
811 cur.push32le(h * 2); // 30: XOR+AND height
812 cur.push16le(1); // 34: number of planes
813 cur.push16le(32); // 36: bits per pixel
814 cur.push32le(0); // 38: Type of compression
815
816 cur.push32le(XORsz + ANDsz);
817 // 42: Size of Image
818 cur.push32le(0); // 46: reserved
819 cur.push32le(0); // 50: reserved
820 cur.push32le(0); // 54: reserved
821 cur.push32le(0); // 58: reserved
822
823 // 62: color data (RGBQUAD icColors[])
824 var y, x;
825 for (y = h - 1; y >= 0; y--) {
826 for (x = 0; x < w; x++) {
827 if (x >= w0 || y >= h0) {
828 cur.push(0); // blue
829 cur.push(0); // green
830 cur.push(0); // red
831 cur.push(0); // alpha
6f4cbb3f 832 } else {
1e13775b
SR
833 var idx = y * Math.ceil(w0 / 8) + Math.floor(x / 8);
834 var alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0;
835 if (cmap) {
836 idx = (w0 * y) + x;
837 var rgb = cmap[pixels[idx]];
838 cur.push(rgb[2]); // blue
839 cur.push(rgb[1]); // green
840 cur.push(rgb[0]); // red
841 cur.push(alpha); // alpha
ec31f82e
SR
842 } else {
843 idx = ((w0 * y) + x) * 4;
a7ca8e5c 844 cur.push(pixels[idx]); // blue
ec31f82e 845 cur.push(pixels[idx + 1]); // green
a7ca8e5c 846 cur.push(pixels[idx + 2]); // red
ec31f82e 847 cur.push(alpha); // alpha
1e13775b 848 }
6f4cbb3f 849 }
2c2b492c
JM
850 }
851 }
2c2b492c 852
1e13775b
SR
853 // XOR/bitmask data (BYTE icXOR[])
854 // (ignored, just needs to be the right size)
855 for (y = 0; y < h; y++) {
856 for (x = 0; x < Math.ceil(w / 8); x++) {
857 cur.push(0);
858 }
9a23006e 859 }
9a23006e 860
1e13775b
SR
861 // AND/bitmask data (BYTE icAND[])
862 // (ignored, just needs to be the right size)
863 for (y = 0; y < h; y++) {
864 for (x = 0; x < Math.ceil(w / 8); x++) {
865 cur.push(0);
866 }
2c2b492c 867 }
2c2b492c 868
1e13775b
SR
869 var url = 'data:image/x-icon;base64,' + Base64.encode(cur);
870 target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
871 };
872})();