diff --git a/1.3.0.tar.gz b/1.3.0.tar.gz new file mode 100644 index 0000000..51a4203 Binary files /dev/null and b/1.3.0.tar.gz differ diff --git a/src/components/GuacClient.vue b/src/components/GuacClient.vue index a1acf4d..8399e5a 100644 --- a/src/components/GuacClient.vue +++ b/src/components/GuacClient.vue @@ -1,286 +1,294 @@ diff --git a/src/lib/Display.js b/src/lib/Display.js new file mode 100644 index 0000000..6beff80 --- /dev/null +++ b/src/lib/Display.js @@ -0,0 +1,1323 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +var Guacamole = Guacamole || {}; +/** + * The Guacamole display. The display does not deal with the Guacamole + * protocol, and instead implements a set of graphical operations which + * embody the set of operations present in the protocol. The order operations + * are executed is guaranteed to be in the same order as their corresponding + * functions are called. + * + * @constructor + */ +Guacamole.Display = function() { + /** + * Reference to this Guacamole.Display. + * @private + */ + var guac_display = this; + var displayWidth = 0; + var displayHeight = 0; + var displayScale = 1; + // Create display + var display = document.createElement("div"); + display.style.position = "relative"; + display.style.width = displayWidth + "px"; + display.style.height = displayHeight + "px"; + // Ensure transformations on display originate at 0,0 + display.style.transformOrigin = display.style.webkitTransformOrigin = display.style.MozTransformOrigin = display.style.OTransformOrigin = display.style.msTransformOrigin = + "0 0"; + // Create default layer + var default_layer = new Guacamole.Display.VisibleLayer( + displayWidth, + displayHeight + ); + // Create cursor layer + var cursor = new Guacamole.Display.VisibleLayer(0, 0); + cursor.setChannelMask(Guacamole.Layer.SRC); + // Add default layer and cursor to display + display.appendChild(default_layer.getElement()); + display.appendChild(cursor.getElement()); + // Create bounding div + var bounds = document.createElement("div"); + bounds.style.position = "relative"; + bounds.style.width = displayWidth * displayScale + "px"; + bounds.style.height = displayHeight * displayScale + "px"; + // Add display to bounds + bounds.appendChild(display); + /** + * The X coordinate of the hotspot of the mouse cursor. The hotspot is + * the relative location within the image of the mouse cursor at which + * each click occurs. + * + * @type {Number} + */ + this.cursorHotspotX = 0; + /** + * The Y coordinate of the hotspot of the mouse cursor. The hotspot is + * the relative location within the image of the mouse cursor at which + * each click occurs. + * + * @type {Number} + */ + this.cursorHotspotY = 0; + /** + * The current X coordinate of the local mouse cursor. This is not + * necessarily the location of the actual mouse - it refers only to + * the location of the cursor image within the Guacamole display, as + * last set by moveCursor(). + * + * @type {Number} + */ + this.cursorX = 0; + /** + * The current X coordinate of the local mouse cursor. This is not + * necessarily the location of the actual mouse - it refers only to + * the location of the cursor image within the Guacamole display, as + * last set by moveCursor(). + * + * @type {Number} + */ + this.cursorY = 0; + /** + * Fired when the default layer (and thus the entire Guacamole display) + * is resized. + * + * @event + * @param {Number} width The new width of the Guacamole display. + * @param {Number} height The new height of the Guacamole display. + */ + this.onresize = null; + /** + * Fired whenever the local cursor image is changed. This can be used to + * implement special handling of the client-side cursor, or to override + * the default use of a software cursor layer. + * + * @event + * @param {HTMLCanvasElement} canvas The cursor image. + * @param {Number} x The X-coordinate of the cursor hotspot. + * @param {Number} y The Y-coordinate of the cursor hotspot. + */ + this.oncursor = null; + /** + * The queue of all pending Tasks. Tasks will be run in order, with new + * tasks added at the end of the queue and old tasks removed from the + * front of the queue (FIFO). These tasks will eventually be grouped + * into a Frame. + * @private + * @type {Task[]} + */ + var tasks = []; + /** + * The queue of all frames. Each frame is a pairing of an array of tasks + * and a callback which must be called when the frame is rendered. + * @private + * @type {Frame[]} + */ + var frames = []; + /** + * Flushes all pending frames. + * @private + */ + function __flush_frames() { + var rendered_frames = 0; + // Draw all pending frames, if ready + while (rendered_frames < frames.length) { + var frame = frames[rendered_frames]; + if (!frame.isReady()) break; + frame.flush(); + rendered_frames++; + } + // Remove rendered frames from array + frames.splice(0, rendered_frames); + } + /** + * An ordered list of tasks which must be executed atomically. Once + * executed, an associated (and optional) callback will be called. + * + * @private + * @constructor + * @param {function} callback The function to call when this frame is + * rendered. + * @param {Task[]} tasks The set of tasks which must be executed to render + * this frame. + */ + function Frame(callback, tasks) { + /** + * Returns whether this frame is ready to be rendered. This function + * returns true if and only if ALL underlying tasks are unblocked. + * + * @returns {Boolean} true if all underlying tasks are unblocked, + * false otherwise. + */ + this.isReady = function() { + // Search for blocked tasks + for (var i = 0; i < tasks.length; i++) { + if (tasks[i].blocked) return false; + } + // If no blocked tasks, the frame is ready + return true; + }; + /** + * Renders this frame, calling the associated callback, if any, after + * the frame is complete. This function MUST only be called when no + * blocked tasks exist. Calling this function with blocked tasks + * will result in undefined behavior. + */ + this.flush = function() { + // Draw all pending tasks. + for (var i = 0; i < tasks.length; i++) tasks[i].execute(); + // Call callback + if (callback) callback(); + }; + } + /** + * A container for an task handler. Each operation which must be ordered + * is associated with a Task that goes into a task queue. Tasks in this + * queue are executed in order once their handlers are set, while Tasks + * without handlers block themselves and any following Tasks from running. + * + * @constructor + * @private + * @param {function} taskHandler The function to call when this task + * runs, if any. + * @param {boolean} blocked Whether this task should start blocked. + */ + function Task(taskHandler, blocked) { + var task = this; + + /** + * Whether this Task is blocked. + * + * @type {boolean} + */ + this.blocked = blocked; + /** + * Unblocks this Task, allowing it to run. + */ + this.unblock = function() { + if (task.blocked) { + task.blocked = false; + __flush_frames(); + } + }; + /** + * Calls the handler associated with this task IMMEDIATELY. This + * function does not track whether this task is marked as blocked. + * Enforcing the blocked status of tasks is up to the caller. + */ + this.execute = function() { + if (taskHandler) taskHandler(); + }; + } + /** + * Schedules a task for future execution. The given handler will execute + * immediately after all previous tasks upon frame flush, unless this + * task is blocked. If any tasks is blocked, the entire frame will not + * render (and no tasks within will execute) until all tasks are unblocked. + * + * @private + * @param {function} handler The function to call when possible, if any. + * @param {boolean} blocked Whether the task should start blocked. + * @returns {Task} The Task created and added to the queue for future + * running. + */ + function scheduleTask(handler, blocked) { + var task = new Task(handler, blocked); + tasks.push(task); + return task; + } + /** + * Returns the element which contains the Guacamole display. + * + * @return {Element} The element containing the Guacamole display. + */ + this.getElement = function() { + return bounds; + }; + /** + * Returns the width of this display. + * + * @return {Number} The width of this display; + */ + this.getWidth = function() { + return displayWidth; + }; + /** + * Returns the height of this display. + * + * @return {Number} The height of this display; + */ + this.getHeight = function() { + return displayHeight; + }; + /** + * Returns the default layer of this display. Each Guacamole display always + * has at least one layer. Other layers can optionally be created within + * this layer, but the default layer cannot be removed and is the absolute + * ancestor of all other layers. + * + * @return {Guacamole.Display.VisibleLayer} The default layer. + */ + this.getDefaultLayer = function() { + return default_layer; + }; + /** + * Returns the cursor layer of this display. Each Guacamole display contains + * a layer for the image of the mouse cursor. This layer is a special case + * and exists above all other layers, similar to the hardware mouse cursor. + * + * @return {Guacamole.Display.VisibleLayer} The cursor layer. + */ + this.getCursorLayer = function() { + return cursor; + }; + /** + * Creates a new layer. The new layer will be a direct child of the default + * layer, but can be moved to be a child of any other layer. Layers returned + * by this function are visible. + * + * @return {Guacamole.Display.VisibleLayer} The newly-created layer. + */ + this.createLayer = function() { + var layer = new Guacamole.Display.VisibleLayer(displayWidth, displayHeight); + layer.move(default_layer, 0, 0, 0); + return layer; + }; + /** + * Creates a new buffer. Buffers are invisible, off-screen surfaces. They + * are implemented in the same manner as layers, but do not provide the + * same nesting semantics. + * + * @return {Guacamole.Layer} The newly-created buffer. + */ + this.createBuffer = function() { + var buffer = new Guacamole.Layer(0, 0); + buffer.autosize = 1; + return buffer; + }; + /** + * Flush all pending draw tasks, if possible, as a new frame. If the entire + * frame is not ready, the flush will wait until all required tasks are + * unblocked. + * + * @param {function} callback The function to call when this frame is + * flushed. This may happen immediately, or + * later when blocked tasks become unblocked. + */ + this.flush = function(callback) { + // Add frame, reset tasks + frames.push(new Frame(callback, tasks)); + tasks = []; + // Attempt flush + __flush_frames(); + }; + /** + * Sets the hotspot and image of the mouse cursor displayed within the + * Guacamole display. + * + * @param {Number} hotspotX The X coordinate of the cursor hotspot. + * @param {Number} hotspotY The Y coordinate of the cursor hotspot. + * @param {Guacamole.Layer} layer The source layer containing the data which + * should be used as the mouse cursor image. + * @param {Number} srcx The X coordinate of the upper-left corner of the + * rectangle within the source layer's coordinate + * space to copy data from. + * @param {Number} srcy The Y coordinate of the upper-left corner of the + * rectangle within the source layer's coordinate + * space to copy data from. + * @param {Number} srcw The width of the rectangle within the source layer's + * coordinate space to copy data from. + * @param {Number} srch The height of the rectangle within the source + * layer's coordinate space to copy data from. + */ + this.setCursor = function(hotspotX, hotspotY, layer, srcx, srcy, srcw, srch) { + scheduleTask(function __display_set_cursor() { + // Set hotspot + guac_display.cursorHotspotX = hotspotX; + guac_display.cursorHotspotY = hotspotY; + // Reset cursor size + cursor.resize(srcw, srch); + // Draw cursor to cursor layer + cursor.copy(layer, srcx, srcy, srcw, srch, 0, 0); + guac_display.moveCursor(guac_display.cursorX, guac_display.cursorY); + // Fire cursor change event + if (guac_display.oncursor) + guac_display.oncursor(cursor.toCanvas(), hotspotX, hotspotY); + }); + }; + /** + * Sets whether the software-rendered cursor is shown. This cursor differs + * from the hardware cursor in that it is built into the Guacamole.Display, + * and relies on its own Guacamole layer to render. + * + * @param {Boolean} [shown=true] Whether to show the software cursor. + */ + this.showCursor = function(shown) { + var element = cursor.getElement(); + var parent = element.parentNode; + // Remove from DOM if hidden + if (shown === false) { + if (parent) parent.removeChild(element); + } + // Otherwise, ensure cursor is child of display + else if (parent !== display) display.appendChild(element); + }; + /** + * Sets the location of the local cursor to the given coordinates. For the + * sake of responsiveness, this function performs its action immediately. + * Cursor motion is not maintained within atomic frames. + * + * @param {Number} x The X coordinate to move the cursor to. + * @param {Number} y The Y coordinate to move the cursor to. + */ + this.moveCursor = function(x, y) { + // Move cursor layer + cursor.translate( + x - guac_display.cursorHotspotX, + y - guac_display.cursorHotspotY + ); + // Update stored position + guac_display.cursorX = x; + guac_display.cursorY = y; + }; + /** + * Changes the size of the given Layer to the given width and height. + * Resizing is only attempted if the new size provided is actually different + * from the current size. + * + * @param {Guacamole.Layer} layer The layer to resize. + * @param {Number} width The new width. + * @param {Number} height The new height. + */ + this.resize = function(layer, width, height) { + scheduleTask(function __display_resize() { + layer.resize(width, height); + // Resize display if default layer is resized + if (layer === default_layer) { + // Update (set) display size + displayWidth = width; + displayHeight = height; + display.style.width = displayWidth + "px"; + display.style.height = displayHeight + "px"; + // Update bounds size + bounds.style.width = displayWidth * displayScale + "px"; + bounds.style.height = displayHeight * displayScale + "px"; + // Notify of resize + if (guac_display.onresize) guac_display.onresize(width, height); + } + }); + }; + /** + * Draws the specified image at the given coordinates. The image specified + * must already be loaded. + * + * @param {Guacamole.Layer} layer + * The layer to draw upon. + * + * @param {Number} x + * The destination X coordinate. + * + * @param {Number} y + * The destination Y coordinate. + * + * @param {CanvasImageSource} image + * The image to draw. Note that this not a URL. + */ + this.drawImage = function(layer, x, y, image) { + scheduleTask(function __display_drawImage() { + layer.drawImage(x, y, image); + }); + }; + /** + * Draws the image contained within the specified Blob at the given + * coordinates. The Blob specified must already be populated with image + * data. + * + * @param {Guacamole.Layer} layer + * The layer to draw upon. + * + * @param {Number} x + * The destination X coordinate. + * + * @param {Number} y + * The destination Y coordinate. + * + * @param {Blob} blob + * The Blob containing the image data to draw. + */ + this.drawBlob = function(layer, x, y, blob) { + var task; + // Prefer createImageBitmap() over blob URLs if available + if (window.createImageBitmap) { + var bitmap; + // Draw image once loaded + task = scheduleTask(function drawImageBitmap() { + layer.drawImage(x, y, bitmap); + }, true); + // Load image from provided blob + window.createImageBitmap(blob).then(function bitmapLoaded(decoded) { + bitmap = decoded; + task.unblock(); + }); + } + // Use blob URLs and the Image object if createImageBitmap() is + // unavailable + else { + // Create URL for blob + var url = URL.createObjectURL(blob); + // Draw and free blob URL when ready + task = scheduleTask(function __display_drawBlob() { + // Draw the image only if it loaded without errors + if (image.width && image.height) layer.drawImage(x, y, image); + // Blob URL no longer needed + URL.revokeObjectURL(url); + }, true); + // Load image from URL + var image = new Image(); + image.onload = task.unblock; + image.onerror = task.unblock; + image.src = url; + } + }; + /** + * Draws the image within the given stream at the given coordinates. The + * image will be loaded automatically, and this and any future operations + * will wait for the image to finish loading. This function will + * automatically choose an approriate method for reading and decoding the + * given image stream, and should be preferred for received streams except + * where manual decoding of the stream is unavoidable. + * + * @param {Guacamole.Layer} layer + * The layer to draw upon. + * + * @param {Number} x + * The destination X coordinate. + * + * @param {Number} y + * The destination Y coordinate. + * + * @param {Guacamole.InputStream} stream + * The stream along which image data will be received. + * + * @param {String} mimetype + * The mimetype of the image within the stream. + */ + + /** + * Draws the image at the specified URL at the given coordinates. The image + * will be loaded automatically, and this and any future operations will + * wait for the image to finish loading. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + * @param {String} url The URL of the image to draw. + */ + this.draw = function(layer, x, y, url) { + var task = scheduleTask(function __display_draw() { + // Draw the image only if it loaded without errors + if (image.width && image.height) layer.drawImage(x, y, image); + }, true); + var image = new Image(); + image.onload = task.unblock; + image.onerror = task.unblock; + image.src = url; + }; + /** + * Plays the video at the specified URL within this layer. The video + * will be loaded automatically, and this and any future operations will + * wait for the video to finish loading. Future operations will not be + * executed until the video finishes playing. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {String} mimetype The mimetype of the video to play. + * @param {Number} duration The duration of the video in milliseconds. + * @param {String} url The URL of the video to play. + */ + this.play = function(layer, mimetype, duration, url) { + // Start loading the video + var video = document.createElement("video"); + video.type = mimetype; + video.src = url; + // Start copying frames when playing + video.addEventListener( + "play", + function() { + function render_callback() { + layer.drawImage(0, 0, video); + if (!video.ended) window.setTimeout(render_callback, 20); + } + + render_callback(); + }, + false + ); + scheduleTask(video.play); + }; + /** + * Transfer a rectangle of image data from one Layer to this Layer using the + * specified transfer function. + * + * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. + * @param {Number} srcx The X coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcy The Y coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcw The width of the rectangle within the source Layer's + * coordinate space to copy data from. + * @param {Number} srch The height of the rectangle within the source + * Layer's coordinate space to copy data from. + * @param {Guacamole.Layer} dstLayer The layer to draw upon. + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + * @param {Function} transferFunction The transfer function to use to + * transfer data from source to + * destination. + */ + this.transfer = function( + srcLayer, + srcx, + srcy, + srcw, + srch, + dstLayer, + x, + y, + transferFunction + ) { + scheduleTask(function __display_transfer() { + dstLayer.transfer( + srcLayer, + srcx, + srcy, + srcw, + srch, + x, + y, + transferFunction + ); + }); + }; + /** + * Put a rectangle of image data from one Layer to this Layer directly + * without performing any alpha blending. Simply copy the data. + * + * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. + * @param {Number} srcx The X coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcy The Y coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcw The width of the rectangle within the source Layer's + * coordinate space to copy data from. + * @param {Number} srch The height of the rectangle within the source + * Layer's coordinate space to copy data from. + * @param {Guacamole.Layer} dstLayer The layer to draw upon. + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + */ + this.put = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { + scheduleTask(function __display_put() { + dstLayer.put(srcLayer, srcx, srcy, srcw, srch, x, y); + }); + }; + /** + * Copy a rectangle of image data from one Layer to this Layer. This + * operation will copy exactly the image data that will be drawn once all + * operations of the source Layer that were pending at the time this + * function was called are complete. This operation will not alter the + * size of the source Layer even if its autosize property is set to true. + * + * @param {Guacamole.Layer} srcLayer The Layer to copy image data from. + * @param {Number} srcx The X coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcy The Y coordinate of the upper-left corner of the + * rectangle within the source Layer's coordinate + * space to copy data from. + * @param {Number} srcw The width of the rectangle within the source Layer's + * coordinate space to copy data from. + * @param {Number} srch The height of the rectangle within the source + * Layer's coordinate space to copy data from. + * @param {Guacamole.Layer} dstLayer The layer to draw upon. + * @param {Number} x The destination X coordinate. + * @param {Number} y The destination Y coordinate. + */ + this.copy = function(srcLayer, srcx, srcy, srcw, srch, dstLayer, x, y) { + scheduleTask(function __display_copy() { + dstLayer.copy(srcLayer, srcx, srcy, srcw, srch, x, y); + }); + }; + /** + * Starts a new path at the specified point. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {Number} x The X coordinate of the point to draw. + * @param {Number} y The Y coordinate of the point to draw. + */ + this.moveTo = function(layer, x, y) { + scheduleTask(function __display_moveTo() { + layer.moveTo(x, y); + }); + }; + /** + * Add the specified line to the current path. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {Number} x The X coordinate of the endpoint of the line to draw. + * @param {Number} y The Y coordinate of the endpoint of the line to draw. + */ + this.lineTo = function(layer, x, y) { + scheduleTask(function __display_lineTo() { + layer.lineTo(x, y); + }); + }; + /** + * Add the specified arc to the current path. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {Number} x The X coordinate of the center of the circle which + * will contain the arc. + * @param {Number} y The Y coordinate of the center of the circle which + * will contain the arc. + * @param {Number} radius The radius of the circle. + * @param {Number} startAngle The starting angle of the arc, in radians. + * @param {Number} endAngle The ending angle of the arc, in radians. + * @param {Boolean} negative Whether the arc should be drawn in order of + * decreasing angle. + */ + this.arc = function(layer, x, y, radius, startAngle, endAngle, negative) { + scheduleTask(function __display_arc() { + layer.arc(x, y, radius, startAngle, endAngle, negative); + }); + }; + /** + * Starts a new path at the specified point. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {Number} cp1x The X coordinate of the first control point. + * @param {Number} cp1y The Y coordinate of the first control point. + * @param {Number} cp2x The X coordinate of the second control point. + * @param {Number} cp2y The Y coordinate of the second control point. + * @param {Number} x The X coordinate of the endpoint of the curve. + * @param {Number} y The Y coordinate of the endpoint of the curve. + */ + this.curveTo = function(layer, cp1x, cp1y, cp2x, cp2y, x, y) { + scheduleTask(function __display_curveTo() { + layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); + }); + }; + /** + * Closes the current path by connecting the end point with the start + * point (if any) with a straight line. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + */ + this.close = function(layer) { + scheduleTask(function __display_close() { + layer.close(); + }); + }; + /** + * Add the specified rectangle to the current path. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {Number} x The X coordinate of the upper-left corner of the + * rectangle to draw. + * @param {Number} y The Y coordinate of the upper-left corner of the + * rectangle to draw. + * @param {Number} w The width of the rectangle to draw. + * @param {Number} h The height of the rectangle to draw. + */ + this.rect = function(layer, x, y, w, h) { + scheduleTask(function __display_rect() { + layer.rect(x, y, w, h); + }); + }; + /** + * Clip all future drawing operations by the current path. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as fillColor()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {Guacamole.Layer} layer The layer to affect. + */ + this.clip = function(layer) { + scheduleTask(function __display_clip() { + layer.clip(); + }); + }; + /** + * Stroke the current path with the specified color. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as clip()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {String} cap The line cap style. Can be "round", "square", + * or "butt". + * @param {String} join The line join style. Can be "round", "bevel", + * or "miter". + * @param {Number} thickness The line thickness in pixels. + * @param {Number} r The red component of the color to fill. + * @param {Number} g The green component of the color to fill. + * @param {Number} b The blue component of the color to fill. + * @param {Number} a The alpha component of the color to fill. + */ + this.strokeColor = function(layer, cap, join, thickness, r, g, b, a) { + scheduleTask(function __display_strokeColor() { + layer.strokeColor(cap, join, thickness, r, g, b, a); + }); + }; + /** + * Fills the current path with the specified color. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as clip()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {Number} r The red component of the color to fill. + * @param {Number} g The green component of the color to fill. + * @param {Number} b The blue component of the color to fill. + * @param {Number} a The alpha component of the color to fill. + */ + this.fillColor = function(layer, r, g, b, a) { + scheduleTask(function __display_fillColor() { + layer.fillColor(r, g, b, a); + }); + }; + /** + * Stroke the current path with the image within the specified layer. The + * image data will be tiled infinitely within the stroke. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as clip()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {String} cap The line cap style. Can be "round", "square", + * or "butt". + * @param {String} join The line join style. Can be "round", "bevel", + * or "miter". + * @param {Number} thickness The line thickness in pixels. + * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern + * within the stroke. + */ + this.strokeLayer = function(layer, cap, join, thickness, srcLayer) { + scheduleTask(function __display_strokeLayer() { + layer.strokeLayer(cap, join, thickness, srcLayer); + }); + }; + /** + * Fills the current path with the image within the specified layer. The + * image data will be tiled infinitely within the stroke. The current path + * is implicitly closed. The current path can continue to be reused + * for other operations (such as clip()) but a new path will be started + * once a path drawing operation (path() or rect()) is used. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + * @param {Guacamole.Layer} srcLayer The layer to use as a repeating pattern + * within the fill. + */ + this.fillLayer = function(layer, srcLayer) { + scheduleTask(function __display_fillLayer() { + layer.fillLayer(srcLayer); + }); + }; + /** + * Push current layer state onto stack. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + */ + this.push = function(layer) { + scheduleTask(function __display_push() { + layer.push(); + }); + }; + /** + * Pop layer state off stack. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + */ + this.pop = function(layer) { + scheduleTask(function __display_pop() { + layer.pop(); + }); + }; + /** + * Reset the layer, clearing the stack, the current path, and any transform + * matrix. + * + * @param {Guacamole.Layer} layer The layer to draw upon. + */ + this.reset = function(layer) { + scheduleTask(function __display_reset() { + layer.reset(); + }); + }; + /** + * Sets the given affine transform (defined with six values from the + * transform's matrix). + * + * @param {Guacamole.Layer} layer The layer to modify. + * @param {Number} a The first value in the affine transform's matrix. + * @param {Number} b The second value in the affine transform's matrix. + * @param {Number} c The third value in the affine transform's matrix. + * @param {Number} d The fourth value in the affine transform's matrix. + * @param {Number} e The fifth value in the affine transform's matrix. + * @param {Number} f The sixth value in the affine transform's matrix. + */ + this.setTransform = function(layer, a, b, c, d, e, f) { + scheduleTask(function __display_setTransform() { + layer.setTransform(a, b, c, d, e, f); + }); + }; + /** + * Applies the given affine transform (defined with six values from the + * transform's matrix). + * + * @param {Guacamole.Layer} layer The layer to modify. + * @param {Number} a The first value in the affine transform's matrix. + * @param {Number} b The second value in the affine transform's matrix. + * @param {Number} c The third value in the affine transform's matrix. + * @param {Number} d The fourth value in the affine transform's matrix. + * @param {Number} e The fifth value in the affine transform's matrix. + * @param {Number} f The sixth value in the affine transform's matrix. + */ + this.transform = function(layer, a, b, c, d, e, f) { + scheduleTask(function __display_transform() { + layer.transform(a, b, c, d, e, f); + }); + }; + /** + * Sets the channel mask for future operations on this Layer. + * + * The channel mask is a Guacamole-specific compositing operation identifier + * with a single bit representing each of four channels (in order): source + * image where destination transparent, source where destination opaque, + * destination where source transparent, and destination where source + * opaque. + * + * @param {Guacamole.Layer} layer The layer to modify. + * @param {Number} mask The channel mask for future operations on this + * Layer. + */ + this.setChannelMask = function(layer, mask) { + scheduleTask(function __display_setChannelMask() { + layer.setChannelMask(mask); + }); + }; + /** + * Sets the miter limit for stroke operations using the miter join. This + * limit is the maximum ratio of the size of the miter join to the stroke + * width. If this ratio is exceeded, the miter will not be drawn for that + * joint of the path. + * + * @param {Guacamole.Layer} layer The layer to modify. + * @param {Number} limit The miter limit for stroke operations using the + * miter join. + */ + this.setMiterLimit = function(layer, limit) { + scheduleTask(function __display_setMiterLimit() { + layer.setMiterLimit(limit); + }); + }; + /** + * Removes the given layer container entirely, such that it is no longer + * contained within its parent layer, if any. + * + * @param {Guacamole.Display.VisibleLayer} layer + * The layer being removed from its parent. + */ + this.dispose = function dispose(layer) { + scheduleTask(function disposeLayer() { + layer.dispose(); + }); + }; + /** + * Applies the given affine transform (defined with six values from the + * transform's matrix) to the given layer. + * + * @param {Guacamole.Display.VisibleLayer} layer + * The layer being distorted. + * + * @param {Number} a + * The first value in the affine transform's matrix. + * + * @param {Number} b + * The second value in the affine transform's matrix. + * + * @param {Number} c + * The third value in the affine transform's matrix. + * + * @param {Number} d + * The fourth value in the affine transform's matrix. + * + * @param {Number} e + * The fifth value in the affine transform's matrix. + * + * @param {Number} f + * The sixth value in the affine transform's matrix. + */ + this.distort = function distort(layer, a, b, c, d, e, f) { + scheduleTask(function distortLayer() { + layer.distort(a, b, c, d, e, f); + }); + }; + /** + * Moves the upper-left corner of the given layer to the given X and Y + * coordinate, sets the Z stacking order, and reparents the layer + * to the given parent layer. + * + * @param {Guacamole.Display.VisibleLayer} layer + * The layer being moved. + * + * @param {Guacamole.Display.VisibleLayer} parent + * The parent to set. + * + * @param {Number} x + * The X coordinate to move to. + * + * @param {Number} y + * The Y coordinate to move to. + * + * @param {Number} z + * The Z coordinate to move to. + */ + this.move = function move(layer, parent, x, y, z) { + scheduleTask(function moveLayer() { + layer.move(parent, x, y, z); + }); + }; + /** + * Sets the opacity of the given layer to the given value, where 255 is + * fully opaque and 0 is fully transparent. + * + * @param {Guacamole.Display.VisibleLayer} layer + * The layer whose opacity should be set. + * + * @param {Number} alpha + * The opacity to set. + */ + this.shade = function shade(layer, alpha) { + scheduleTask(function shadeLayer() { + layer.shade(alpha); + }); + }; + /** + * Sets the scale of the client display element such that it renders at + * a relatively smaller or larger size, without affecting the true + * resolution of the display. + * + * @param {Number} scale The scale to resize to, where 1.0 is normal + * size (1:1 scale). + */ + this.scale = function(scale) { + display.style.transform = display.style.WebkitTransform = display.style.MozTransform = display.style.OTransform = display.style.msTransform = + "scale(" + scale + "," + scale + ")"; + displayScale = scale; + // Update bounds size + bounds.style.width = displayWidth * displayScale + "px"; + bounds.style.height = displayHeight * displayScale + "px"; + }; + /** + * Returns the scale of the display. + * + * @return {Number} The scale of the display. + */ + this.getScale = function() { + return displayScale; + }; + /** + * Returns a canvas element containing the entire display, with all child + * layers composited within. + * + * @return {HTMLCanvasElement} A new canvas element containing a copy of + * the display. + */ + this.flatten = function() { + // Get destination canvas + var canvas = document.createElement("canvas"); + canvas.width = default_layer.width; + canvas.height = default_layer.height; + var context = canvas.getContext("2d"); + // Returns sorted array of children + function get_children(layer) { + // Build array of children + var children = []; + for (var index in layer.children) children.push(layer.children[index]); + // Sort + children.sort(function children_comparator(a, b) { + // Compare based on Z order + var diff = a.z - b.z; + if (diff !== 0) return diff; + // If Z order identical, use document order + var a_element = a.getElement(); + var b_element = b.getElement(); + var position = b_element.compareDocumentPosition(a_element); + if (position & Node.DOCUMENT_POSITION_PRECEDING) return -1; + if (position & Node.DOCUMENT_POSITION_FOLLOWING) return 1; + // Otherwise, assume same + return 0; + }); + // Done + return children; + } + // Draws the contents of the given layer at the given coordinates + function draw_layer(layer, x, y) { + // Draw layer + if (layer.width > 0 && layer.height > 0) { + // Save and update alpha + var initial_alpha = context.globalAlpha; + context.globalAlpha *= layer.alpha / 255.0; + // Copy data + context.drawImage(layer.getCanvas(), x, y); + // Draw all children + var children = get_children(layer); + for (var i = 0; i < children.length; i++) { + var child = children[i]; + draw_layer(child, x + child.x, y + child.y); + } + // Restore alpha + context.globalAlpha = initial_alpha; + } + } + // Draw default layer and all children + draw_layer(default_layer, 0, 0); + // Return new canvas copy + return canvas; + }; +}; +/** + * Simple container for Guacamole.Layer, allowing layers to be easily + * repositioned and nested. This allows certain operations to be accelerated + * through DOM manipulation, rather than raster operations. + * + * @constructor + * @augments Guacamole.Layer + * @param {Number} width The width of the Layer, in pixels. The canvas element + * backing this Layer will be given this width. + * @param {Number} height The height of the Layer, in pixels. The canvas element + * backing this Layer will be given this height. + */ +Guacamole.Display.VisibleLayer = function(width, height) { + Guacamole.Layer.apply(this, [width, height]); + /** + * Reference to this layer. + * @private + */ + var layer = this; + /** + * Identifier which uniquely identifies this layer. This is COMPLETELY + * UNRELATED to the index of the underlying layer, which is specific + * to the Guacamole protocol, and not relevant at this level. + * + * @private + * @type {Number} + */ + this.__unique_id = Guacamole.Display.VisibleLayer.__next_id++; + /** + * The opacity of the layer container, where 255 is fully opaque and 0 is + * fully transparent. + */ + this.alpha = 0xff; + /** + * X coordinate of the upper-left corner of this layer container within + * its parent, in pixels. + * @type {Number} + */ + this.x = 0; + /** + * Y coordinate of the upper-left corner of this layer container within + * its parent, in pixels. + * @type {Number} + */ + this.y = 0; + /** + * Z stacking order of this layer relative to other sibling layers. + * @type {Number} + */ + this.z = 0; + /** + * The affine transformation applied to this layer container. Each element + * corresponds to a value from the transformation matrix, with the first + * three values being the first row, and the last three values being the + * second row. There are six values total. + * + * @type {Number[]} + */ + this.matrix = [1, 0, 0, 1, 0, 0]; + /** + * The parent layer container of this layer, if any. + * @type {Guacamole.Display.VisibleLayer} + */ + this.parent = null; + /** + * Set of all children of this layer, indexed by layer index. This object + * will have one property per child. + */ + this.children = {}; + // Set layer position + var canvas = layer.getCanvas(); + canvas.style.position = "absolute"; + canvas.style.left = "0px"; + canvas.style.top = "0px"; + // Create div with given size + var div = document.createElement("div"); + div.appendChild(canvas); + div.style.width = width + "px"; + div.style.height = height + "px"; + div.style.position = "absolute"; + div.style.left = "0px"; + div.style.top = "0px"; + div.style.overflow = "hidden"; + /** + * Superclass resize() function. + * @private + */ + var __super_resize = this.resize; + this.resize = function(width, height) { + // Resize containing div + div.style.width = width + "px"; + div.style.height = height + "px"; + __super_resize(width, height); + }; + + /** + * Returns the element containing the canvas and any other elements + * associated with this layer. + * @returns {Element} The element containing this layer's canvas. + */ + this.getElement = function() { + return div; + }; + /** + * The translation component of this layer's transform. + * @private + */ + var translate = "translate(0px, 0px)"; // (0, 0) + /** + * The arbitrary matrix component of this layer's transform. + * @private + */ + var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity + /** + * Moves the upper-left corner of this layer to the given X and Y + * coordinate. + * + * @param {Number} x The X coordinate to move to. + * @param {Number} y The Y coordinate to move to. + */ + this.translate = function(x, y) { + layer.x = x; + layer.y = y; + // Generate translation + translate = "translate(" + x + "px," + y + "px)"; + // Set layer transform + div.style.transform = div.style.WebkitTransform = div.style.MozTransform = div.style.OTransform = div.style.msTransform = + translate + " " + matrix; + }; + /** + * Moves the upper-left corner of this VisibleLayer to the given X and Y + * coordinate, sets the Z stacking order, and reparents this VisibleLayer + * to the given VisibleLayer. + * + * @param {Guacamole.Display.VisibleLayer} parent The parent to set. + * @param {Number} x The X coordinate to move to. + * @param {Number} y The Y coordinate to move to. + * @param {Number} z The Z coordinate to move to. + */ + this.move = function(parent, x, y, z) { + // Set parent if necessary + if (layer.parent !== parent) { + // Maintain relationship + if (layer.parent) delete layer.parent.children[layer.__unique_id]; + layer.parent = parent; + parent.children[layer.__unique_id] = layer; + // Reparent element + var parent_element = parent.getElement(); + parent_element.appendChild(div); + } + // Set location + layer.translate(x, y); + layer.z = z; + div.style.zIndex = z; + }; + /** + * Sets the opacity of this layer to the given value, where 255 is fully + * opaque and 0 is fully transparent. + * + * @param {Number} a The opacity to set. + */ + this.shade = function(a) { + layer.alpha = a; + div.style.opacity = a / 255.0; + }; + /** + * Removes this layer container entirely, such that it is no longer + * contained within its parent layer, if any. + */ + this.dispose = function() { + // Remove from parent container + if (layer.parent) { + delete layer.parent.children[layer.__unique_id]; + layer.parent = null; + } + // Remove from parent element + if (div.parentNode) div.parentNode.removeChild(div); + }; + /** + * Applies the given affine transform (defined with six values from the + * transform's matrix). + * + * @param {Number} a The first value in the affine transform's matrix. + * @param {Number} b The second value in the affine transform's matrix. + * @param {Number} c The third value in the affine transform's matrix. + * @param {Number} d The fourth value in the affine transform's matrix. + * @param {Number} e The fifth value in the affine transform's matrix. + * @param {Number} f The sixth value in the affine transform's matrix. + */ + this.distort = function(a, b, c, d, e, f) { + // Store matrix + layer.matrix = [a, b, c, d, e, f]; + // Generate matrix transformation + matrix = + /* a c e + * b d f + * 0 0 1 + */ + + "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")"; + // Set layer transform + div.style.transform = div.style.WebkitTransform = div.style.MozTransform = div.style.OTransform = div.style.msTransform = + translate + " " + matrix; + }; +}; +/** + * The next identifier to be assigned to the layer container. This identifier + * uniquely identifies each VisibleLayer, but is unrelated to the index of + * the layer, which exists at the protocol/client level only. + * + * @private + * @type {Number} + */ +Guacamole.Display.VisibleLayer.__next_id = 0; diff --git a/src/lib/GuacTouchpad.js b/src/lib/GuacTouchpad.js new file mode 100644 index 0000000..3e8e68b --- /dev/null +++ b/src/lib/GuacTouchpad.js @@ -0,0 +1,217 @@ +import Guacamole from "guacamole-common-js"; + +const touchpad = function(element) { + /** + * Reference to this Guacamole.Mouse.Touchpad. + * @private + */ + var guac_touchpad = this; + /** + * The distance a two-finger touch must move per scrollwheel event, in + * pixels. + */ + this.scrollThreshold = 20 * (window.devicePixelRatio || 1); + /** + * The maximum number of milliseconds to wait for a touch to end for the + * gesture to be considered a click. + */ + this.clickTimingThreshold = 250; + /** + * The maximum number of pixels to allow a touch to move for the gesture to + * be considered a click. + */ + this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1); + /** + * The current mouse state. The properties of this state are updated when + * mouse events fire. This state object is also passed in as a parameter to + * the handler of any mouse events. + * + * @type {Guacamole.Mouse.State} + */ + this.currentState = new Guacamole.Mouse.State( + 0, + 0, + false, + false, + false, + false, + false + ); + /** + * Fired whenever a mouse button is effectively pressed. This can happen + * as part of a "click" gesture initiated by the user by tapping one + * or more fingers over the touchpad element, as part of a "scroll" + * gesture initiated by dragging two fingers up or down, etc. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousedown = null; + /** + * Fired whenever a mouse button is effectively released. This can happen + * as part of a "click" gesture initiated by the user by tapping one + * or more fingers over the touchpad element, as part of a "scroll" + * gesture initiated by dragging two fingers up or down, etc. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmouseup = null; + /** + * Fired whenever the user moves the mouse by dragging their finger over + * the touchpad element. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousemove = null; + var touch_count = 0; + var last_touch_x = 0; + var last_touch_y = 0; + var last_touch_time = 0; + var pixels_moved = 0; + var touch_buttons = { + 1: "left", + 2: "right", + 3: "middle", + }; + var gesture_in_progress = false; + var click_release_timeout = null; + element.addEventListener( + "touchend", + function(e) { + e.preventDefault(); + + // If we're handling a gesture AND this is the last touch + if (gesture_in_progress && e.touches.length === 0) { + var time = new Date().getTime(); + // Get corresponding mouse button + var button = touch_buttons[touch_count]; + // If mouse already down, release anad clear timeout + if (guac_touchpad.currentState[button]) { + // Fire button up event + guac_touchpad.currentState[button] = false; + if (guac_touchpad.onmouseup) + guac_touchpad.onmouseup(guac_touchpad.currentState); + // Clear timeout, if set + if (click_release_timeout) { + window.clearTimeout(click_release_timeout); + click_release_timeout = null; + } + } + // If single tap detected (based on time and distance) + if ( + time - last_touch_time <= guac_touchpad.clickTimingThreshold && + pixels_moved < guac_touchpad.clickMoveThreshold + ) { + // Fire button down event + guac_touchpad.currentState[button] = true; + if (guac_touchpad.onmousedown) + guac_touchpad.onmousedown(guac_touchpad.currentState); + // Delay mouse up - mouse up should be canceled if + // touchstart within timeout. + click_release_timeout = window.setTimeout(function() { + // Fire button up event + guac_touchpad.currentState[button] = false; + if (guac_touchpad.onmouseup) + guac_touchpad.onmouseup(guac_touchpad.currentState); + + // Gesture now over + gesture_in_progress = false; + }, guac_touchpad.clickTimingThreshold); + } + // If we're not waiting to see if this is a click, stop gesture + if (!click_release_timeout) gesture_in_progress = false; + } + }, + false + ); + element.addEventListener( + "touchstart", + function(e) { + e.preventDefault(); + // Track number of touches, but no more than three + touch_count = Math.min(e.touches.length, 3); + // Clear timeout, if set + if (click_release_timeout) { + window.clearTimeout(click_release_timeout); + click_release_timeout = null; + } + // Record initial touch location and time for touch movement + // and tap gestures + if (!gesture_in_progress) { + // Stop mouse events while touching + gesture_in_progress = true; + // Record touch location and time + var starting_touch = e.touches[0]; + last_touch_x = starting_touch.clientX; + last_touch_y = starting_touch.clientY; + last_touch_time = new Date().getTime(); + pixels_moved = 0; + } + }, + false + ); + element.addEventListener( + "touchmove", + function(e) { + e.preventDefault(); + // Get change in touch location + var touch = e.touches[0]; + var delta_x = touch.clientX - last_touch_x; + var delta_y = touch.clientY - last_touch_y; + // Track pixels moved + pixels_moved += Math.abs(delta_x) + Math.abs(delta_y); + // If only one touch involved, this is mouse move + if (touch_count === 1) { + // Calculate average velocity in Manhatten pixels per millisecond + var velocity = pixels_moved / (new Date().getTime() - last_touch_time); + // Scale mouse movement relative to velocity + var scale = 1 + velocity; + // Update mouse location + guac_touchpad.currentState.x += delta_x * scale; + guac_touchpad.currentState.y += delta_y * scale; + // Prevent mouse from leaving screen + if (guac_touchpad.currentState.x < 0) guac_touchpad.currentState.x = 0; + else if (guac_touchpad.currentState.x >= element.offsetWidth) + guac_touchpad.currentState.x = element.offsetWidth - 1; + if (guac_touchpad.currentState.y < 0) guac_touchpad.currentState.y = 0; + else if (guac_touchpad.currentState.y >= element.offsetHeight) + guac_touchpad.currentState.y = element.offsetHeight - 1; + // Fire movement event, if defined + if (guac_touchpad.onmousemove) + guac_touchpad.onmousemove(guac_touchpad.currentState); + // Update touch location + last_touch_x = touch.clientX; + last_touch_y = touch.clientY; + } + // Interpret two-finger swipe as scrollwheel + else if (touch_count === 2) { + // If change in location passes threshold for scroll + if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { + // Decide button based on Y movement direction + var button; + if (delta_y > 0) button = "down"; + else button = "up"; + // Fire button down event + guac_touchpad.currentState[button] = true; + if (guac_touchpad.onmousedown) + guac_touchpad.onmousedown(guac_touchpad.currentState); + // Fire button up event + guac_touchpad.currentState[button] = false; + if (guac_touchpad.onmouseup) + guac_touchpad.onmouseup(guac_touchpad.currentState); + // Only update touch location after a scroll has been + // detected + last_touch_x = touch.clientX; + last_touch_y = touch.clientY; + } + } + }, + false + ); +}; + +export default { + touchpad, +}; diff --git a/src/lib/GuacTouchscreen.js b/src/lib/GuacTouchscreen.js new file mode 100644 index 0000000..302b89c --- /dev/null +++ b/src/lib/GuacTouchscreen.js @@ -0,0 +1,280 @@ +import Guacamole from "guacamole-common-js"; + +const touchscreen = function(element) { + /** + * Reference to this Guacamole.Mouse.Touchscreen. + * @private + */ + var guac_touchscreen = this; + /** + * Whether a gesture is known to be in progress. If false, touch events + * will be ignored. + * + * @private + */ + var gesture_in_progress = false; + /** + * The start X location of a gesture. + * @private + */ + var gesture_start_x = null; + /** + * The start Y location of a gesture. + * @private + */ + var gesture_start_y = null; + /** + * The timeout associated with the delayed, cancellable click release. + * + * @private + */ + var click_release_timeout = null; + /** + * The timeout associated with long-press for right click. + * + * @private + */ + var long_press_timeout = null; + /** + * The distance a two-finger touch must move per scrollwheel event, in + * pixels. + */ + this.scrollThreshold = 20 * (window.devicePixelRatio || 1); + /** + * The maximum number of milliseconds to wait for a touch to end for the + * gesture to be considered a click. + */ + this.clickTimingThreshold = 250; + /** + * The maximum number of pixels to allow a touch to move for the gesture to + * be considered a click. + */ + this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1); + /** + * The amount of time a press must be held for long press to be + * detected. + */ + this.longPressThreshold = 500; + /** + * The current mouse state. The properties of this state are updated when + * mouse events fire. This state object is also passed in as a parameter to + * the handler of any mouse events. + * + * @type {Guacamole.Mouse.State} + */ + this.currentState = new Guacamole.Mouse.State( + 0, + 0, + false, + false, + false, + false, + false + ); + /** + * Fired whenever a mouse button is effectively pressed. This can happen + * as part of a "mousedown" gesture initiated by the user by pressing one + * finger over the touchscreen element, as part of a "scroll" gesture + * initiated by dragging two fingers up or down, etc. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousedown = null; + /** + * Fired whenever a mouse button is effectively released. This can happen + * as part of a "mouseup" gesture initiated by the user by removing the + * finger pressed against the touchscreen element, or as part of a "scroll" + * gesture initiated by dragging two fingers up or down, etc. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmouseup = null; + /** + * Fired whenever the user moves the mouse by dragging their finger over + * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad, + * dragging a finger over the touchscreen element will always cause + * the mouse button to be effectively down, as if clicking-and-dragging. + * + * @event + * @param {Guacamole.Mouse.State} state The current mouse state. + */ + this.onmousemove = null; + /** + * Presses the given mouse button, if it isn't already pressed. Valid + * button values are "left", "middle", "right", "up", and "down". + * + * @private + * @param {String} button The mouse button to press. + */ + function press_button(button) { + if (!guac_touchscreen.currentState[button]) { + guac_touchscreen.currentState[button] = true; + if (guac_touchscreen.onmousedown) + guac_touchscreen.onmousedown(guac_touchscreen.currentState); + } + } + /** + * Releases the given mouse button, if it isn't already released. Valid + * button values are "left", "middle", "right", "up", and "down". + * + * @private + * @param {String} button The mouse button to release. + */ + function release_button(button) { + if (guac_touchscreen.currentState[button]) { + guac_touchscreen.currentState[button] = false; + if (guac_touchscreen.onmouseup) + guac_touchscreen.onmouseup(guac_touchscreen.currentState); + } + } + /** + * Clicks (presses and releases) the given mouse button. Valid button + * values are "left", "middle", "right", "up", and "down". + * + * @private + * @param {String} button The mouse button to click. + */ + function click_button(button) { + press_button(button); + release_button(button); + } + /** + * Moves the mouse to the given coordinates. These coordinates must be + * relative to the browser window, as they will be translated based on + * the touch event target's location within the browser window. + * + * @private + * @param {Number} x The X coordinate of the mouse pointer. + * @param {Number} y The Y coordinate of the mouse pointer. + */ + function move_mouse(x, y) { + guac_touchscreen.currentState.fromClientPosition(element, x, y); + if (guac_touchscreen.onmousemove) + guac_touchscreen.onmousemove(guac_touchscreen.currentState); + } + /** + * Returns whether the given touch event exceeds the movement threshold for + * clicking, based on where the touch gesture began. + * + * @private + * @param {TouchEvent} e The touch event to check. + * @return {Boolean} true if the movement threshold is exceeded, false + * otherwise. + */ + function finger_moved(e) { + var touch = e.touches[0] || e.changedTouches[0]; + var delta_x = touch.clientX - gesture_start_x; + var delta_y = touch.clientY - gesture_start_y; + return ( + Math.sqrt(delta_x * delta_x + delta_y * delta_y) >= + guac_touchscreen.clickMoveThreshold + ); + } + /** + * Begins a new gesture at the location of the first touch in the given + * touch event. + * + * @private + * @param {TouchEvent} e The touch event beginning this new gesture. + */ + function begin_gesture(e) { + var touch = e.touches[0]; + gesture_in_progress = true; + gesture_start_x = touch.clientX; + gesture_start_y = touch.clientY; + } + /** + * End the current gesture entirely. Wait for all touches to be done before + * resuming gesture detection. + * + * @private + */ + function end_gesture() { + window.clearTimeout(click_release_timeout); + window.clearTimeout(long_press_timeout); + gesture_in_progress = false; + } + element.addEventListener( + "touchend", + function(e) { + // Do not handle if no gesture + if (!gesture_in_progress) return; + // Ignore if more than one touch + if (e.touches.length !== 0 || e.changedTouches.length !== 1) { + end_gesture(); + return; + } + // Long-press, if any, is over + window.clearTimeout(long_press_timeout); + // Always release mouse button if pressed + release_button("left"); + // If finger hasn't moved enough to cancel the click + if (!finger_moved(e)) { + e.preventDefault(); + // If not yet pressed, press and start delay release + if (!guac_touchscreen.currentState.left) { + var touch = e.changedTouches[0]; + move_mouse(touch.clientX, touch.clientY); + press_button("left"); + // Release button after a delay, if not canceled + click_release_timeout = window.setTimeout(function() { + release_button("left"); + end_gesture(); + }, guac_touchscreen.clickTimingThreshold); + } + } // end if finger not moved + }, + false + ); + element.addEventListener( + "touchstart", + function(e) { + // Ignore if more than one touch + if (e.touches.length !== 1) { + end_gesture(); + return; + } + e.preventDefault(); + // New touch begins a new gesture + begin_gesture(e); + // Keep button pressed if tap after left click + window.clearTimeout(click_release_timeout); + // Click right button if this turns into a long-press + long_press_timeout = window.setTimeout(function() { + var touch = e.touches[0]; + move_mouse(touch.clientX, touch.clientY); + click_button("right"); + end_gesture(); + }, guac_touchscreen.longPressThreshold); + }, + false + ); + element.addEventListener( + "touchmove", + function(e) { + // Do not handle if no gesture + if (!gesture_in_progress) return; + // Cancel long press if finger moved + if (finger_moved(e)) window.clearTimeout(long_press_timeout); + // Ignore if more than one touch + if (e.touches.length !== 1) { + end_gesture(); + return; + } + // Update mouse position if dragging + if (guac_touchscreen.currentState.left) { + e.preventDefault(); + // Update state + var touch = e.touches[0]; + move_mouse(touch.clientX, touch.clientY); + } + }, + false + ); +}; + +export default { + touchscreen, +};