Initial refactor of raster operations to real transfer functions.
[guacamole-common-js.git] / src / main / resources / guacamole.js
1
2 /* ***** BEGIN LICENSE BLOCK *****
3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4  *
5  * The contents of this file are subject to the Mozilla Public License Version
6  * 1.1 (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  * http://www.mozilla.org/MPL/
9  *
10  * Software distributed under the License is distributed on an "AS IS" basis,
11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12  * for the specific language governing rights and limitations under the
13  * License.
14  *
15  * The Original Code is guacamole-common-js.
16  *
17  * The Initial Developer of the Original Code is
18  * Michael Jumper.
19  * Portions created by the Initial Developer are Copyright (C) 2010
20  * the Initial Developer. All Rights Reserved.
21  *
22  * Contributor(s):
23  *
24  * Alternatively, the contents of this file may be used under the terms of
25  * either the GNU General Public License Version 2 or later (the "GPL"), or
26  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27  * in which case the provisions of the GPL or the LGPL are applicable instead
28  * of those above. If you wish to allow use of your version of this file only
29  * under the terms of either the GPL or the LGPL, and not to allow others to
30  * use your version of this file under the terms of the MPL, indicate your
31  * decision by deleting the provisions above and replace them with the notice
32  * and other provisions required by the GPL or the LGPL. If you do not delete
33  * the provisions above, a recipient may use your version of this file under
34  * the terms of any one of the MPL, the GPL or the LGPL.
35  *
36  * ***** END LICENSE BLOCK ***** */
37
38 // Guacamole namespace
39 var Guacamole = Guacamole || {};
40
41
42 /**
43  * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
44  * automatically handles incoming and outgoing Guacamole instructions via the
45  * provided tunnel, updating the display using one or more canvas elements.
46  * 
47  * @constructor
48  * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
49  *                                  Guacamole instructions.
50  */
51 Guacamole.Client = function(tunnel) {
52
53     var guac_client = this;
54
55     var STATE_IDLE          = 0;
56     var STATE_CONNECTING    = 1;
57     var STATE_WAITING       = 2;
58     var STATE_CONNECTED     = 3;
59     var STATE_DISCONNECTING = 4;
60     var STATE_DISCONNECTED  = 5;
61
62     var currentState = STATE_IDLE;
63     
64     var currentTimestamp = 0;
65     var pingInterval = null;
66
67     var displayWidth = 0;
68     var displayHeight = 0;
69
70     /**
71      * Map of all Guacamole binary raster operations to transfer functions.
72      * @private
73      */
74     var transferFunctions = {
75
76         0x10: function (src, dst) { return 0x00;         }, /* BLACK */
77         0x1F: function (src, dst) { return 0xFF;         }, /* WHITE */
78
79         0x13: function (src, dst) { return src;          }, /* SRC */
80         0x15: function (src, dst) { return dst;          }, /* DEST */
81         0x1C: function (src, dst) { return ~src;         }, /* NSRC */
82         0x1A: function (src, dst) { return ~dst;         }, /* NDEST */
83
84         0x11: function (src, dst) { return src & dst;    }, /* AND */
85         0x1E: function (src, dst) { return ~(src & dst); }, /* NAND */
86
87         0x17: function (src, dst) { return src | dst;    }, /* OR */
88         0x18: function (src, dst) { return ~(src | dst); }, /* NOR */
89
90         0x16: function (src, dst) { return src ^ dst;    }, /* XOR */
91         0x19: function (src, dst) { return ~(src ^ dst); }, /* XNOR */
92
93         0x14: function (src, dst) { return ~src & dst;   }, /* AND inverted source */
94         0x1D: function (src, dst) { return ~src | dst;   }, /* OR inverted source */
95         0x12: function (src, dst) { return src & ~dst;   }, /* AND inverted destination */
96         0x1B: function (src, dst) { return src | ~dst;   }  /* OR inverted destination */
97
98     };
99
100     // Create display
101     var display = document.createElement("div");
102     display.style.position = "relative";
103     display.style.width = displayWidth + "px";
104     display.style.height = displayHeight + "px";
105
106     // Create default layer
107     var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
108
109     // Position default layer
110     var default_layer_container_element = default_layer_container.getElement();
111     default_layer_container_element.style.position = "absolute";
112     default_layer_container_element.style.left = "0px";
113     default_layer_container_element.style.top  = "0px";
114
115     // Create cursor layer
116     var cursor = new Guacamole.Client.LayerContainer(0, 0);
117     cursor.getLayer().setChannelMask(Guacamole.Layer.SRC);
118
119     // Position cursor layer
120     var cursor_element = cursor.getElement();
121     cursor_element.style.position = "absolute";
122     cursor_element.style.left = "0px";
123     cursor_element.style.top  = "0px";
124
125     // Add default layer and cursor to display
126     display.appendChild(default_layer_container.getElement());
127     display.appendChild(cursor.getElement());
128
129     // Initially, only default layer exists
130     var layers =  [default_layer_container];
131
132     // No initial buffers
133     var buffers = [];
134
135     tunnel.onerror = function(message) {
136         if (guac_client.onerror)
137             guac_client.onerror(message);
138     };
139
140     function setState(state) {
141         if (state != currentState) {
142             currentState = state;
143             if (guac_client.onstatechange)
144                 guac_client.onstatechange(currentState);
145         }
146     }
147
148     function isConnected() {
149         return currentState == STATE_CONNECTED
150             || currentState == STATE_WAITING;
151     }
152
153     var cursorHotspotX = 0;
154     var cursorHotspotY = 0;
155
156     var cursorX = 0;
157     var cursorY = 0;
158
159     function moveCursor(x, y) {
160
161         var element = cursor.getElement();
162
163         // Update rect
164         element.style.left = (x - cursorHotspotX) + "px";
165         element.style.top  = (y - cursorHotspotY) + "px";
166
167         // Update stored position
168         cursorX = x;
169         cursorY = y;
170
171     }
172
173     guac_client.getDisplay = function() {
174         return display;
175     };
176
177     guac_client.sendKeyEvent = function(pressed, keysym) {
178         // Do not send requests if not connected
179         if (!isConnected())
180             return;
181
182         tunnel.sendMessage("key", keysym, pressed);
183     };
184
185     guac_client.sendMouseState = function(mouseState) {
186
187         // Do not send requests if not connected
188         if (!isConnected())
189             return;
190
191         // Update client-side cursor
192         moveCursor(
193             mouseState.x,
194             mouseState.y
195         );
196
197         // Build mask
198         var buttonMask = 0;
199         if (mouseState.left)   buttonMask |= 1;
200         if (mouseState.middle) buttonMask |= 2;
201         if (mouseState.right)  buttonMask |= 4;
202         if (mouseState.up)     buttonMask |= 8;
203         if (mouseState.down)   buttonMask |= 16;
204
205         // Send message
206         tunnel.sendMessage("mouse", mouseState.x, mouseState.y, buttonMask);
207     };
208
209     guac_client.setClipboard = function(data) {
210
211         // Do not send requests if not connected
212         if (!isConnected())
213             return;
214
215         tunnel.sendMessage("clipboard", data);
216     };
217
218     // Handlers
219     guac_client.onstatechange = null;
220     guac_client.onname = null;
221     guac_client.onerror = null;
222     guac_client.onclipboard = null;
223
224     // Layers
225     function getBufferLayer(index) {
226
227         index = -1 - index;
228         var buffer = buffers[index];
229
230         // Create buffer if necessary
231         if (buffer == null) {
232             buffer = new Guacamole.Layer(0, 0);
233             buffer.autosize = 1;
234             buffers[index] = buffer;
235         }
236
237         return buffer;
238
239     }
240
241     function getLayerContainer(index) {
242
243         var layer = layers[index];
244         if (layer == null) {
245
246             // Add new layer
247             layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight);
248             layers[index] = layer;
249
250             // Get and position layer
251             var layer_element = layer.getElement();
252             layer_element.style.position = "absolute";
253             layer_element.style.left = "0px";
254             layer_element.style.top = "0px";
255
256             // Add to default layer container
257             default_layer_container.getElement().appendChild(layer_element);
258
259         }
260
261         return layer;
262
263     }
264
265     function getLayer(index) {
266        
267         // If buffer, just get layer
268         if (index < 0)
269             return getBufferLayer(index);
270
271         // Otherwise, retrieve layer from layer container
272         return getLayerContainer(index).getLayer();
273
274     }
275
276     var instructionHandlers = {
277
278         "error": function(parameters) {
279             if (guac_client.onerror) guac_client.onerror(parameters[0]);
280             guac_client.disconnect();
281         },
282
283         "name": function(parameters) {
284             if (guac_client.onname) guac_client.onname(parameters[0]);
285         },
286
287         "clipboard": function(parameters) {
288             if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
289         },
290
291         "size": function(parameters) {
292
293             var layer_index = parseInt(parameters[0]);
294             var width = parseInt(parameters[1]);
295             var height = parseInt(parameters[2]);
296
297             // Only valid for layers (buffers auto-resize)
298             if (layer_index >= 0) {
299
300                 // Resize layer
301                 var layer_container = getLayerContainer(layer_index);
302                 layer_container.resize(width, height);
303
304                 // If layer is default, resize display
305                 if (layer_index == 0) {
306
307                     displayWidth = width;
308                     displayHeight = height;
309
310                     // Update (set) display size
311                     display.style.width = displayWidth + "px";
312                     display.style.height = displayHeight + "px";
313
314                 }
315
316             } // end if layer (not buffer)
317
318         },
319
320         "move": function(parameters) {
321             
322             var layer_index = parseInt(parameters[0]);
323             var parent_index = parseInt(parameters[1]);
324             var x = parseInt(parameters[2]);
325             var y = parseInt(parameters[3]);
326             var z = parseInt(parameters[4]);
327
328             // Only valid for non-default layers
329             if (layer_index > 0 && parent_index >= 0) {
330
331                 // Get container element
332                 var layer_container = getLayerContainer(layer_index).getElement();
333                 var parent = getLayerContainer(parent_index).getElement();
334
335                 // Set parent if necessary
336                 if (!(layer_container.parentNode === parent))
337                     parent.appendChild(layer_container);
338
339                 // Move layer
340                 layer_container.style.left   = x + "px";
341                 layer_container.style.top    = y + "px";
342                 layer_container.style.zIndex = z;
343
344             }
345
346         },
347
348         "dispose": function(parameters) {
349             
350             var layer_index = parseInt(parameters[0]);
351
352             // If visible layer, remove from parent
353             if (layer_index > 0) {
354
355                 // Get container element
356                 var layer_container = getLayerContainer(layer_index).getElement();
357
358                 // Remove from parent
359                 layer_container.parentNode.removeChild(layer_container);
360
361                 // Delete reference
362                 delete layers[layer_index];
363
364             }
365
366             // If buffer, just delete reference
367             else if (layer_index < 0)
368                 delete buffers[-1 - layer_index];
369
370             // Attempting to dispose the root layer currently has no effect.
371
372         },
373
374         "png": function(parameters) {
375
376             var channelMask = parseInt(parameters[0]);
377             var layer = getLayer(parseInt(parameters[1]));
378             var x = parseInt(parameters[2]);
379             var y = parseInt(parameters[3]);
380             var data = parameters[4];
381
382             layer.setChannelMask(channelMask);
383
384             layer.draw(
385                 x,
386                 y,
387                 "data:image/png;base64," + data
388             );
389
390             // If received first update, no longer waiting.
391             if (currentState == STATE_WAITING)
392                 setState(STATE_CONNECTED);
393
394         },
395
396         "copy": function(parameters) {
397
398             var srcL = getLayer(parseInt(parameters[0]));
399             var srcX = parseInt(parameters[1]);
400             var srcY = parseInt(parameters[2]);
401             var srcWidth = parseInt(parameters[3]);
402             var srcHeight = parseInt(parameters[4]);
403             var channelMask = parseInt(parameters[5]);
404             var dstL = getLayer(parseInt(parameters[6]));
405             var dstX = parseInt(parameters[7]);
406             var dstY = parseInt(parameters[8]);
407
408             dstL.setChannelMask(channelMask);
409
410             dstL.copyRect(
411                 srcL,
412                 srcX,
413                 srcY,
414                 srcWidth, 
415                 srcHeight, 
416                 dstX,
417                 dstY 
418             );
419
420         },
421
422         "transfer": function(parameters) {
423
424             var srcL = getLayer(parseInt(parameters[0]));
425             var srcX = parseInt(parameters[1]);
426             var srcY = parseInt(parameters[2]);
427             var srcWidth = parseInt(parameters[3]);
428             var srcHeight = parseInt(parameters[4]);
429             var transferFunction = transferFunctions[parameters[5]];
430             var dstL = getLayer(parseInt(parameters[6]));
431             var dstX = parseInt(parameters[7]);
432             var dstY = parseInt(parameters[8]);
433
434             dstL.transfer(
435                 srcL,
436                 srcX,
437                 srcY,
438                 srcWidth, 
439                 srcHeight, 
440                 dstX,
441                 dstY,
442                 transferFunction
443             );
444
445         },
446
447         "rect": function(parameters) {
448
449             var channelMask = parseInt(parameters[0]);
450             var layer = getLayer(parseInt(parameters[1]));
451             var x = parseInt(parameters[2]);
452             var y = parseInt(parameters[3]);
453             var w = parseInt(parameters[4]);
454             var h = parseInt(parameters[5]);
455             var r = parseInt(parameters[6]);
456             var g = parseInt(parameters[7]);
457             var b = parseInt(parameters[8]);
458             var a = parseInt(parameters[9]);
459
460             layer.setChannelMask(channelMask);
461
462             layer.drawRect(
463                 x, y, w, h,
464                 r, g, b, a
465             );
466
467         },
468
469         "clip": function(parameters) {
470
471             var layer = getLayer(parseInt(parameters[0]));
472             var x = parseInt(parameters[1]);
473             var y = parseInt(parameters[2]);
474             var w = parseInt(parameters[3]);
475             var h = parseInt(parameters[4]);
476
477             layer.clipRect(x, y, w, h);
478
479         },
480
481         "cursor": function(parameters) {
482
483             cursorHotspotX = parseInt(parameters[0]);
484             cursorHotspotY = parseInt(parameters[1]);
485             var srcL = getLayer(parseInt(parameters[2]));
486             var srcX = parseInt(parameters[3]);
487             var srcY = parseInt(parameters[4]);
488             var srcWidth = parseInt(parameters[5]);
489             var srcHeight = parseInt(parameters[6]);
490
491             // Reset cursor size
492             cursor.resize(srcWidth, srcHeight);
493
494             // Draw cursor to cursor layer
495             cursor.getLayer().copyRect(
496                 srcL,
497                 srcX,
498                 srcY,
499                 srcWidth, 
500                 srcHeight, 
501                 0,
502                 0 
503             );
504
505             // Update cursor position (hotspot may have changed)
506             moveCursor(cursorX, cursorY);
507
508         },
509
510         "sync": function(parameters) {
511
512             var timestamp = parameters[0];
513
514             // When all layers have finished rendering all instructions
515             // UP TO THIS POINT IN TIME, send sync response.
516
517             var layersToSync = 0;
518             function syncLayer() {
519
520                 layersToSync--;
521
522                 // Send sync response when layers are finished
523                 if (layersToSync == 0) {
524                     if (timestamp != currentTimestamp) {
525                         tunnel.sendMessage("sync", timestamp);
526                         currentTimestamp = timestamp;
527                     }
528                 }
529
530             }
531
532             // Count active, not-ready layers and install sync tracking hooks
533             for (var i=0; i<layers.length; i++) {
534
535                 var layer = layers[i].getLayer();
536                 if (layer && !layer.isReady()) {
537                     layersToSync++;
538                     layer.sync(syncLayer);
539                 }
540
541             }
542
543             // If all layers are ready, then we didn't install any hooks.
544             // Send sync message now,
545             if (layersToSync == 0) {
546                 if (timestamp != currentTimestamp) {
547                     tunnel.sendMessage("sync", timestamp);
548                     currentTimestamp = timestamp;
549                 }
550             }
551
552         }
553       
554     };
555
556
557     tunnel.oninstruction = function(opcode, parameters) {
558
559         var handler = instructionHandlers[opcode];
560         if (handler)
561             handler(parameters);
562
563     };
564
565
566     guac_client.disconnect = function() {
567
568         // Only attempt disconnection not disconnected.
569         if (currentState != STATE_DISCONNECTED
570                 && currentState != STATE_DISCONNECTING) {
571
572             setState(STATE_DISCONNECTING);
573
574             // Stop ping
575             if (pingInterval)
576                 window.clearInterval(pingInterval);
577
578             // Send disconnect message and disconnect
579             tunnel.sendMessage("disconnect");
580             tunnel.disconnect();
581             setState(STATE_DISCONNECTED);
582
583         }
584
585     };
586     
587     guac_client.connect = function(data) {
588
589         setState(STATE_CONNECTING);
590
591         try {
592             tunnel.connect(data);
593         }
594         catch (e) {
595             setState(STATE_IDLE);
596             throw e;
597         }
598
599         // Ping every 5 seconds (ensure connection alive)
600         pingInterval = window.setInterval(function() {
601             tunnel.sendMessage("sync", currentTimestamp);
602         }, 5000);
603
604         setState(STATE_WAITING);
605     };
606
607 };
608
609
610 /**
611  * Simple container for Guacamole.Layer, allowing layers to be easily
612  * repositioned and nested. This allows certain operations to be accelerated
613  * through DOM manipulation, rather than raster operations.
614  * 
615  * @constructor
616  * 
617  * @param {Number} width The width of the Layer, in pixels. The canvas element
618  *                       backing this Layer will be given this width.
619  *                       
620  * @param {Number} height The height of the Layer, in pixels. The canvas element
621  *                        backing this Layer will be given this height.
622  */
623 Guacamole.Client.LayerContainer = function(width, height) {
624
625     /**
626      * Reference to this LayerContainer.
627      * @private
628      */
629     var layer_container = this;
630
631     // Create layer with given size
632     var layer = new Guacamole.Layer(width, height);
633
634     // Set layer position
635     var canvas = layer.getCanvas();
636     canvas.style.position = "absolute";
637     canvas.style.left = "0px";
638     canvas.style.top = "0px";
639
640     // Create div with given size
641     var div = document.createElement("div");
642     div.appendChild(canvas);
643     div.style.width = width + "px";
644     div.style.height = height + "px";
645
646     /**
647      * Changes the size of this LayerContainer and the contained Layer to the
648      * given width and height.
649      * 
650      * @param {Number} width The new width to assign to this Layer.
651      * @param {Number} height The new height to assign to this Layer.
652      */
653     layer_container.resize = function(width, height) {
654
655         // Resize layer
656         layer.resize(width, height);
657
658         // Resize containing div
659         div.style.width = width + "px";
660         div.style.height = height + "px";
661
662     };
663   
664     /**
665      * Returns the Layer contained within this LayerContainer.
666      * @returns {Guacamole.Layer} The Layer contained within this LayerContainer.
667      */
668     layer_container.getLayer = function() {
669         return layer;
670     };
671
672     /**
673      * Returns the element containing the Layer within this LayerContainer.
674      * @returns {Element} The element containing the Layer within this LayerContainer.
675      */
676     layer_container.getElement = function() {
677         return div;
678     };
679
680 };