text: /*
   * Copyright (C) 2013 Glyptodon LLC
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to deal
   * in the Software without restriction, including without limitation the rights
   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   * copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
   * THE SOFTWARE.
   */
  
  var Guacamole = Guacamole || {};
  
  /**
   * Provides cross-browser mouse events for a given element. The events of
   * the given element are automatically populated with handlers that translate
   * mouse events into a non-browser-specific event provided by the
   * Guacamole.Mouse instance.
   * 
   * @constructor
   * @param {Element} element The Element to use to provide mouse events.
   */
  Guacamole.Mouse = function(element) {
  
      /**
       * Reference to this Guacamole.Mouse.
       * @private
       */
      var guac_mouse = this;
  
      /**
       * The number of mousemove events to require before re-enabling mouse
       * event handling after receiving a touch event.
       */
      this.touchMouseThreshold = 3;
  
      /**
       * The minimum amount of pixels scrolled required for a single scroll button
       * click.
       */
      this.scrollThreshold = 120;
  
      /**
       * The number of pixels to scroll per line.
       */
      this.PIXELS_PER_LINE = 40;
  
      /**
       * The number of pixels to scroll per page.
       */
      this.PIXELS_PER_PAGE = 640;
  
      /**
       * 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 the user presses a mouse button down over the element
       * associated with this Guacamole.Mouse.
       * 
       * @event
       * @param {Guacamole.Mouse.State} state The current mouse state.
       */
  	this.onmousedown = null;
  
      /**
       * Fired whenever the user releases a mouse button down over the element
       * associated with this Guacamole.Mouse.
       * 
       * @event
       * @param {Guacamole.Mouse.State} state The current mouse state.
       */
  	this.onmouseup = null;
  
      /**
       * Fired whenever the user moves the mouse over the element associated with
       * this Guacamole.Mouse.
       * 
       * @event
       * @param {Guacamole.Mouse.State} state The current mouse state.
       */
  	this.onmousemove = null;
  
      /**
       * Fired whenever the mouse leaves the boundaries of the element associated
       * with this Guacamole.Mouse.
       * 
       * @event
       */
  	this.onmouseout = null;
  
      /**
       * Counter of mouse events to ignore. This decremented by mousemove, and
       * while non-zero, mouse events will have no effect.
       * @private
       */
      var ignore_mouse = 0;
  
      /**
       * Cumulative scroll delta amount. This value is accumulated through scroll
       * events and results in scroll button clicks if it exceeds a certain
       * threshold.
       */
      var scroll_delta = 0;
  
      function cancelEvent(e) {
          e.stopPropagation();
          if (e.preventDefault) e.preventDefault();
          e.returnValue = false;
      }
  
      // Block context menu so right-click gets sent properly
      element.addEventListener("contextmenu", function(e) {
          cancelEvent(e);
      }, false);
  
      element.addEventListener("mousemove", function(e) {
  
          cancelEvent(e);
  
          // If ignoring events, decrement counter
          if (ignore_mouse) {
              ignore_mouse--;
              return;
          }
  
          guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY);
  
          if (guac_mouse.onmousemove)
              guac_mouse.onmousemove(guac_mouse.currentState);
  
      }, false);
  
      element.addEventListener("mousedown", function(e) {
  
          cancelEvent(e);
  
          // Do not handle if ignoring events
          if (ignore_mouse)
              return;
  
          switch (e.button) {
              case 0:
                  guac_mouse.currentState.left = true;
                  break;
              case 1:
                  guac_mouse.currentState.middle = true;
                  break;
              case 2:
                  guac_mouse.currentState.right = true;
                  break;
          }
  
          if (guac_mouse.onmousedown)
              guac_mouse.onmousedown(guac_mouse.currentState);
  
      }, false);
  
      element.addEventListener("mouseup", function(e) {
  
          cancelEvent(e);
  
          // Do not handle if ignoring events
          if (ignore_mouse)
              return;
  
          switch (e.button) {
              case 0:
                  guac_mouse.currentState.left = false;
                  break;
              case 1:
                  guac_mouse.currentState.middle = false;
                  break;
              case 2:
                  guac_mouse.currentState.right = false;
                  break;
          }
  
          if (guac_mouse.onmouseup)
              guac_mouse.onmouseup(guac_mouse.currentState);
  
      }, false);
  
      element.addEventListener("mouseout", function(e) {
  
          // Get parent of the element the mouse pointer is leaving
         	if (!e) e = window.event;
  
          // Check that mouseout is due to actually LEAVING the element
          var target = e.relatedTarget || e.toElement;
          while (target) {
              if (target === element)
                  return;
              target = target.parentNode;
          }
  
          cancelEvent(e);
  
          // Release all buttons
          if (guac_mouse.currentState.left
              || guac_mouse.currentState.middle
              || guac_mouse.currentState.right) {
  
              guac_mouse.currentState.left = false;
              guac_mouse.currentState.middle = false;
              guac_mouse.currentState.right = false;
  
              if (guac_mouse.onmouseup)
                  guac_mouse.onmouseup(guac_mouse.currentState);
          }
  
          // Fire onmouseout event
          if (guac_mouse.onmouseout)
              guac_mouse.onmouseout();
  
      }, false);
  
      // Override selection on mouse event element.
      element.addEventListener("selectstart", function(e) {
          cancelEvent(e);
      }, false);
  
      // Ignore all pending mouse events when touch events are the apparent source
      function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; }
  
      element.addEventListener("touchmove",  ignorePendingMouseEvents, false);
      element.addEventListener("touchstart", ignorePendingMouseEvents, false);
      element.addEventListener("touchend",   ignorePendingMouseEvents, false);
  
      // Scroll wheel support
      function mousewheel_handler(e) {
  
          // Determine approximate scroll amount (in pixels)
          var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta;
  
          // If successfully retrieved scroll amount, convert to pixels if not
          // already in pixels
          if (delta) {
  
              // Convert to pixels if delta was lines
              if (e.deltaMode === 1)
                  delta = e.deltaY * guac_mouse.PIXELS_PER_LINE;
  
              // Convert to pixels if delta was pages
              else if (e.deltaMode === 2)
                  delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE;
  
          }
  
          // Otherwise, assume legacy mousewheel event and line scrolling
          else
              delta = e.detail * guac_mouse.PIXELS_PER_LINE;
          
          // Update overall delta
          scroll_delta += delta;
  
          // Up
          while (scroll_delta <= -guac_mouse.scrollThreshold) {
  
              if (guac_mouse.onmousedown) {
                  guac_mouse.currentState.up = true;
                  guac_mouse.onmousedown(guac_mouse.currentState);
              }
  
              if (guac_mouse.onmouseup) {
                  guac_mouse.currentState.up = false;
                  guac_mouse.onmouseup(guac_mouse.currentState);
              }
  
              scroll_delta += guac_mouse.scrollThreshold;
  
          }
  
          // Down
          while (scroll_delta >= guac_mouse.scrollThreshold) {
  
              if (guac_mouse.onmousedown) {
                  guac_mouse.currentState.down = true;
                  guac_mouse.onmousedown(guac_mouse.currentState);
              }
  
              if (guac_mouse.onmouseup) {
                  guac_mouse.currentState.down = false;
                  guac_mouse.onmouseup(guac_mouse.currentState);
              }
  
              scroll_delta -= guac_mouse.scrollThreshold;
  
          }
  
          cancelEvent(e);
  
      }
  
      element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
      element.addEventListener('mousewheel',     mousewheel_handler, false);
      element.addEventListener('wheel',          mousewheel_handler, false);
  
      /**
       * Whether the browser supports CSS3 cursor styling, including hotspot
       * coordinates.
       *
       * @private
       * @type Boolean
       */
      var CSS3_CURSOR_SUPPORTED = (function() {
  
          var div = document.createElement("div");
  
          // If no cursor property at all, then no support
          if (!("cursor" in div.style))
              return false;
  
          try {
              // Apply simple 1x1 PNG
              div.style.cursor = "url(data:image/png;base64,"
                               + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB"
                               + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI"
                               + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA"
                               + "AABJRU5ErkJggg==) 0 0, auto";
          }
          catch (e) {
              return false;
          }
  
          // Verify cursor property is set to URL with hotspot
          return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || "");
  
      })();
  
      /**
       * Changes the local mouse cursor to the given canvas, having the given
       * hotspot coordinates. This affects styling of the element backing this
       * Guacamole.Mouse only, and may fail depending on browser support for
       * setting the mouse cursor.
       * 
       * If setting the local cursor is desired, it is up to the implementation
       * to do something else, such as use the software cursor built into
       * Guacamole.Display, if the local cursor cannot be set.
       *
       * @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.
       * @return {Boolean} true if the cursor was successfully set, false if the
       *                   cursor could not be set for any reason.
       */
      this.setCursor = function(canvas, x, y) {
  
          // Attempt to set via CSS3 cursor styling
          if (CSS3_CURSOR_SUPPORTED) {
              var dataURL = canvas.toDataURL('image/png');
              element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto";
              return true;
          }
  
          // Otherwise, setting cursor failed
          return false;
  
      };
  
  };
  
  /**
   * Simple container for properties describing the state of a mouse.
   * 
   * @constructor
   * @param {Number} x The X position of the mouse pointer in pixels.
   * @param {Number} y The Y position of the mouse pointer in pixels.
   * @param {Boolean} left Whether the left mouse button is pressed. 
   * @param {Boolean} middle Whether the middle mouse button is pressed. 
   * @param {Boolean} right Whether the right mouse button is pressed. 
   * @param {Boolean} up Whether the up mouse button is pressed (the fourth
   *                     button, usually part of a scroll wheel). 
   * @param {Boolean} down Whether the down mouse button is pressed (the fifth
   *                       button, usually part of a scroll wheel). 
   */
  Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) {
  
      /**
       * Reference to this Guacamole.Mouse.State.
       * @private
       */
      var guac_state = this;
  
      /**
       * The current X position of the mouse pointer.
       * @type Number
       */
      this.x = x;
  
      /**
       * The current Y position of the mouse pointer.
       * @type Number
       */
      this.y = y;
  
      /**
       * Whether the left mouse button is currently pressed.
       * @type Boolean
       */
      this.left = left;
  
      /**
       * Whether the middle mouse button is currently pressed.
       * @type Boolean
       */
      this.middle = middle;
  
      /**
       * Whether the right mouse button is currently pressed.
       * @type Boolean
       */
      this.right = right;
  
      /**
       * Whether the up mouse button is currently pressed. This is the fourth
       * mouse button, associated with upward scrolling of the mouse scroll
       * wheel.
       * @type Boolean
       */
      this.up = up;
  
      /**
       * Whether the down mouse button is currently pressed. This is the fifth 
       * mouse button, associated with downward scrolling of the mouse scroll
       * wheel.
       * @type Boolean
       */
      this.down = down;
  
      /**
       * Updates the position represented within this state object by the given
       * element and clientX/clientY coordinates (commonly available within event
       * objects). Position is translated from clientX/clientY (relative to
       * viewport) to element-relative coordinates.
       * 
       * @param {Element} element The element the coordinates should be relative
       *                          to.
       * @param {Number} clientX The X coordinate to translate, viewport-relative.
       * @param {Number} clientY The Y coordinate to translate, viewport-relative.
       */
      this.fromClientPosition = function(element, clientX, clientY) {
      
          guac_state.x = clientX - element.offsetLeft;
          guac_state.y = clientY - element.offsetTop;
  
          // This is all JUST so we can get the mouse position within the element
          var parent = element.offsetParent;
          while (parent && !(parent === document.body)) {
              guac_state.x -= parent.offsetLeft - parent.scrollLeft;
              guac_state.y -= parent.offsetTop  - parent.scrollTop;
  
              parent = parent.offsetParent;
          }
  
          // Element ultimately depends on positioning within document body,
          // take document scroll into account. 
          if (parent) {
              var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
              var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
  
              guac_state.x -= parent.offsetLeft - documentScrollLeft;
              guac_state.y -= parent.offsetTop  - documentScrollTop;
          }
  
      };
  
  };
  
  /**
   * Provides cross-browser relative touch event translation for a given element.
   * 
   * Touch events are translated into mouse events as if the touches occurred
   * on a touchpad (drag to push the mouse pointer, tap to click).
   * 
   * @constructor
   * @param {Element} element The Element to use to provide touch events.
   */
  Guacamole.Mouse.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);
  
  };
  
  /**
   * Provides cross-browser absolute touch event translation for a given element.
   *
   * Touch events are translated into mouse events as if the touches occurred
   * on a touchscreen (tapping anywhere on the screen clicks at that point,
   * long-press to right-click).
   *
   * @constructor
   * @param {Element} element The Element to use to provide touch events.
   */
  Guacamole.Mouse.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.
       */
      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.
       */
      var click_release_timeout = null;
  
      /**
       * The timeout associated with long-press for right click.
       */
      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);
  
  };
  