dom/browser-element/BrowserElementPanning.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

     1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set ts=2 sw=2 sts=2 et: */
     4 /* This Source Code Form is subject to the terms of the Mozilla Public
     5  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     6  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     8 "use strict";
     9 dump("############################### browserElementPanning.js loaded\n");
    11 let { classes: Cc, interfaces: Ci, results: Cr, utils: Cu }  = Components;
    12 Cu.import("resource://gre/modules/Services.jsm");
    13 Cu.import("resource://gre/modules/Geometry.jsm");
    15 var global = this;
    17 const kObservedEvents = [
    18   "BEC:ShownModalPrompt",
    19   "Activity:Success",
    20   "Activity:Error"
    21 ];
    23 const ContentPanning = {
    24   // Are we listening to touch or mouse events?
    25   watchedEventsType: '',
    27   // Are mouse events being delivered to this content along with touch
    28   // events, in violation of spec?
    29   hybridEvents: false,
    31   init: function cp_init() {
    32     // If APZ is enabled, we do active element handling in C++
    33     // (see widget/xpwidgets/ActiveElementManager.h), and panning
    34     // itself in APZ, so we don't need to handle any touch events here.
    35     if (docShell.asyncPanZoomEnabled === false) {
    36       this._setupListenersForPanning();
    37     }
    39     addEventListener("unload",
    40 		     this._unloadHandler.bind(this),
    41 		     /* useCapture = */ false,
    42 		     /* wantsUntrusted = */ false);
    44     addMessageListener("Viewport:Change", this._recvViewportChange.bind(this));
    45     addMessageListener("Gesture:DoubleTap", this._recvDoubleTap.bind(this));
    46     addEventListener("visibilitychange", this._handleVisibilityChange.bind(this));
    47     kObservedEvents.forEach((topic) => {
    48       Services.obs.addObserver(this, topic, false);
    49     });
    50   },
    52   _setupListenersForPanning: function cp_setupListenersForPanning() {
    53     var events;
    54     try {
    55       content.document.createEvent('TouchEvent');
    56       events = ['touchstart', 'touchend', 'touchmove'];
    57       this.watchedEventsType = 'touch';
    58 #ifdef MOZ_WIDGET_GONK
    59       // The gonk widget backend does not deliver mouse events per
    60       // spec.  Third-party content isn't exposed to this behavior,
    61       // but that behavior creates some extra work for us here.
    62       let appInfo = Cc["@mozilla.org/xre/app-info;1"];
    63       let isParentProcess =
    64         !appInfo || appInfo.getService(Ci.nsIXULRuntime)
    65                            .processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
    66       this.hybridEvents = isParentProcess;
    67 #endif
    68     } catch(e) {
    69       // Touch events aren't supported, so fall back on mouse.
    70       events = ['mousedown', 'mouseup', 'mousemove'];
    71       this.watchedEventsType = 'mouse';
    72     }
    74     let els = Cc["@mozilla.org/eventlistenerservice;1"]
    75                 .getService(Ci.nsIEventListenerService);
    77     events.forEach(function(type) {
    78       // Using the system group for mouse/touch events to avoid
    79       // missing events if .stopPropagation() has been called.
    80       els.addSystemEventListener(global, type,
    81                                  this.handleEvent.bind(this),
    82                                  /* useCapture = */ false);
    83     }.bind(this));
    84   },
    86   handleEvent: function cp_handleEvent(evt) {
    87     // Ignore events targeting a <iframe mozbrowser> since those will be
    88     // handle by the BrowserElementPanning.js instance of it.
    89     if (evt.target instanceof Ci.nsIMozBrowserFrame) {
    90       return;
    91     }
    93     if (evt.defaultPrevented || evt.multipleActionsPrevented) {
    94       // clean up panning state even if touchend/mouseup has been preventDefault.
    95       if(evt.type === 'touchend' || evt.type === 'mouseup') {
    96         if (this.dragging &&
    97             (this.watchedEventsType === 'mouse' ||
    98              this.findPrimaryPointer(evt.changedTouches))) {
    99           this._finishPanning();
   100         }
   101       }
   102       return;
   103     }
   105     switch (evt.type) {
   106       case 'mousedown':
   107       case 'touchstart':
   108         this.onTouchStart(evt);
   109         break;
   110       case 'mousemove':
   111       case 'touchmove':
   112         this.onTouchMove(evt);
   113         break;
   114       case 'mouseup':
   115       case 'touchend':
   116         this.onTouchEnd(evt);
   117         break;
   118       case 'click':
   119         evt.stopPropagation();
   120         evt.preventDefault();
   122         let target = evt.target;
   123         let view = target.ownerDocument ? target.ownerDocument.defaultView
   124                                         : target;
   125         view.removeEventListener('click', this, true, true);
   126         break;
   127     }
   128   },
   130   observe: function cp_observe(subject, topic, data) {
   131     this._resetHover();
   132   },
   134   position: new Point(0 , 0),
   136   findPrimaryPointer: function cp_findPrimaryPointer(touches) {
   137     if (!('primaryPointerId' in this))
   138       return null;
   140     for (let i = 0; i < touches.length; i++) {
   141       if (touches[i].identifier === this.primaryPointerId) {
   142         return touches[i];
   143       }
   144     }
   145     return null;
   146   },
   148   onTouchStart: function cp_onTouchStart(evt) {
   149     let screenX, screenY;
   150     if (this.watchedEventsType == 'touch') {
   151       if ('primaryPointerId' in this || evt.touches.length >= 2) {
   152         this._resetActive();
   153         return;
   154       }
   156       let firstTouch = evt.changedTouches[0];
   157       this.primaryPointerId = firstTouch.identifier;
   158       this.pointerDownTarget = firstTouch.target;
   159       screenX = firstTouch.screenX;
   160       screenY = firstTouch.screenY;
   161     } else {
   162       this.pointerDownTarget = evt.target;
   163       screenX = evt.screenX;
   164       screenY = evt.screenY;
   165     }
   166     this.dragging = true;
   167     this.panning = false;
   169     let oldTarget = this.target;
   170     [this.target, this.scrollCallback] = this.getPannable(this.pointerDownTarget);
   172     // If we have a pointer down target, we may need to fill in for EventStateManager
   173     // in setting the active state on the target element.  Set a timer to
   174     // ensure the pointer-down target is active.  (If it's already
   175     // active, the timer is a no-op.)
   176     if (this.pointerDownTarget !== null) {
   177       // If there's no possibility this is a drag/pan, activate now.
   178       // Otherwise wait a little bit to see if the gesture isn't a
   179       // tap.
   180       if (this.target === null) {
   181         this.notify(this._activationTimer);
   182       } else {
   183         this._activationTimer.initWithCallback(this,
   184                                                this._activationDelayMs,
   185                                                Ci.nsITimer.TYPE_ONE_SHOT);
   186       }
   187     }
   189     // If there is a pan animation running (from a previous pan gesture) and
   190     // the user touch back the screen, stop this animation immediatly and
   191     // prevent the possible click action if the touch happens on the same
   192     // target.
   193     this.preventNextClick = false;
   194     if (KineticPanning.active) {
   195       KineticPanning.stop();
   197       if (oldTarget && oldTarget == this.target)
   198         this.preventNextClick = true;
   199     }
   201     this.position.set(screenX, screenY);
   202     KineticPanning.reset();
   203     KineticPanning.record(new Point(0, 0), evt.timeStamp);
   205     // We prevent start events to avoid sending a focus event at the end of this
   206     // touch series. See bug 889717.
   207     if ((this.panning || this.preventNextClick)) {
   208       evt.preventDefault();
   209     }
   210   },
   212   onTouchEnd: function cp_onTouchEnd(evt) {
   213     let touch = null;
   214     if (!this.dragging ||
   215         (this.watchedEventsType == 'touch' &&
   216          !(touch = this.findPrimaryPointer(evt.changedTouches)))) {
   217       return;
   218     }
   220     // !isPan() and evt.detail should always give the same answer here
   221     // since they use the same heuristics, but use the native gecko
   222     // computation when possible.
   223     //
   224     // NB: when we're using touch events, then !KineticPanning.isPan()
   225     // => this.panning, so we'll never attempt to block the click
   226     // event.  That's OK however, because we won't fire a synthetic
   227     // click when we're using touch events and this touch series
   228     // wasn't a "tap" gesture.
   229     let click = (this.watchedEventsType == 'mouse') ?
   230       evt.detail : !KineticPanning.isPan();
   231     // Additionally, if we're seeing non-compliant hybrid events, a
   232     // "real" click will be generated if we started and ended on the
   233     // same element.
   234     if (this.hybridEvents) {
   235       let target =
   236         content.document.elementFromPoint(touch.clientX, touch.clientY);
   237       click |= (target === this.pointerDownTarget);
   238     }
   240     if (this.target && click && (this.panning || this.preventNextClick)) {
   241       if (this.hybridEvents) {
   242         let target = this.target;
   243         let view = target.ownerDocument ? target.ownerDocument.defaultView
   244                                         : target;
   245         view.addEventListener('click', this, true, true);
   246       } else {
   247         // We prevent end events to avoid sending a focus event. See bug 889717.
   248         evt.preventDefault();
   249       }
   250     } else if (this.target && click && !this.panning) {
   251       this.notify(this._activationTimer);
   252     }
   254     this._finishPanning();
   256     // Now that we're done, avoid entraining the thing we just panned.
   257     this.pointerDownTarget = null;
   258   },
   260   onTouchMove: function cp_onTouchMove(evt) {
   261     if (!this.dragging)
   262       return;
   264     let screenX, screenY;
   265     if (this.watchedEventsType == 'touch') {
   266       let primaryTouch = this.findPrimaryPointer(evt.changedTouches);
   267       if (evt.touches.length > 1 || !primaryTouch)
   268         return;
   269       screenX = primaryTouch.screenX;
   270       screenY = primaryTouch.screenY;
   271     } else {
   272       screenX = evt.screenX;
   273       screenY = evt.screenY;
   274     }
   276     let current = this.position;
   277     let delta = new Point(screenX - current.x, screenY - current.y);
   278     current.set(screenX, screenY);
   280     KineticPanning.record(delta, evt.timeStamp);
   282     let isPan = KineticPanning.isPan();
   284     // If we've detected a pan gesture, cancel the active state of the
   285     // current target.
   286     if (!this.panning && isPan) {
   287       this._resetActive();
   288     }
   290     // There's no possibility of us panning anything.
   291     if (!this.scrollCallback) {
   292       return;
   293     }
   295     // Scroll manually.
   296     this.scrollCallback(delta.scale(-1));
   298     if (!this.panning && isPan) {
   299       this.panning = true;
   300       this._activationTimer.cancel();
   301     }
   303     if (this.panning) {
   304       // Only do this when we're actually executing a pan gesture.
   305       // Otherwise synthetic mouse events will be canceled.
   306       evt.stopPropagation();
   307       evt.preventDefault();
   308     }
   309   },
   311   // nsITimerCallback
   312   notify: function cp_notify(timer) {
   313     this._setActive(this.pointerDownTarget);
   314   },
   316   onKineticBegin: function cp_onKineticBegin(evt) {
   317   },
   319   onKineticPan: function cp_onKineticPan(delta) {
   320     return !this.scrollCallback(delta);
   321   },
   323   onKineticEnd: function cp_onKineticEnd() {
   324     if (!this.dragging)
   325       this.scrollCallback = null;
   326   },
   328   getPannable: function cp_getPannable(node) {
   329     let pannableNode = this._findPannable(node);
   330     if (pannableNode) {
   331       return [pannableNode, this._generateCallback(pannableNode)];
   332     }
   334     return [null, null];
   335   },
   337   _findPannable: function cp_findPannable(node) {
   338     if (!(node instanceof Ci.nsIDOMHTMLElement) || node.tagName == 'HTML') {
   339       return null;
   340     }
   342     let nodeContent = node.ownerDocument.defaultView;
   343     while (!(node instanceof Ci.nsIDOMHTMLBodyElement)) {
   344       let style = nodeContent.getComputedStyle(node, null);
   346       let overflow = [style.getPropertyValue('overflow'),
   347                       style.getPropertyValue('overflow-x'),
   348                       style.getPropertyValue('overflow-y')];
   350       let rect = node.getBoundingClientRect();
   351       let isAuto = (overflow.indexOf('auto') != -1 &&
   352                    (rect.height < node.scrollHeight ||
   353                     rect.width < node.scrollWidth));
   355       let isScroll = (overflow.indexOf('scroll') != -1);
   357       let isScrollableTextarea = (node.tagName == 'TEXTAREA' &&
   358           (node.scrollHeight > node.clientHeight ||
   359            node.scrollWidth > node.clientWidth ||
   360            ('scrollLeftMax' in node && node.scrollLeftMax > 0) ||
   361            ('scrollTopMax' in node && node.scrollTopMax > 0)));
   362       if (isScroll || isAuto || isScrollableTextarea) {
   363         return node;
   364       }
   366       node = node.parentNode;
   367     }
   369     if (nodeContent.scrollMaxX || nodeContent.scrollMaxY) {
   370       return nodeContent;
   371     }
   373     if (nodeContent.frameElement) {
   374       return this._findPannable(nodeContent.frameElement);
   375     }
   377     return null;
   378   },
   380   _generateCallback: function cp_generateCallback(root) {
   381     let firstScroll = true;
   382     let target;
   383     let current;
   384     let win, doc, htmlNode, bodyNode;
   386     function doScroll(node, delta) {
   387       if (node instanceof Ci.nsIDOMHTMLElement) {
   388         return node.scrollByNoFlush(delta.x, delta.y);
   389       } else if (node instanceof Ci.nsIDOMWindow) {
   390         win = node;
   391         doc = win.document;
   393         // "overflow:hidden" on either the <html> or the <body> node should
   394         // prevent the user from scrolling the root viewport.
   395         if (doc instanceof Ci.nsIDOMHTMLDocument) {
   396           htmlNode = doc.documentElement;
   397           bodyNode = doc.body;
   398           if (win.getComputedStyle(htmlNode, null).overflowX == "hidden" ||
   399               win.getComputedStyle(bodyNode, null).overflowX == "hidden") {
   400             delta.x = 0;
   401           }
   402           if (win.getComputedStyle(htmlNode, null).overflowY == "hidden" ||
   403               win.getComputedStyle(bodyNode, null).overflowY == "hidden") {
   404             delta.y = 0;
   405           }
   406         }
   407         let oldX = node.scrollX;
   408         let oldY = node.scrollY;
   409         node.scrollBy(delta.x, delta.y);
   410         return (node.scrollX != oldX || node.scrollY != oldY);
   411       }
   412       // If we get here, |node| isn't an HTML element and it's not a window,
   413       // but findPannable apparently thought it was scrollable... What is it?
   414       return false;
   415     }
   417     function targetParent(node) {
   418       return node.parentNode || node.frameElement || null;
   419     }
   421     function scroll(delta) {
   422       current = root;
   423       firstScroll = true;
   424       while (current) {
   425         if (doScroll(current, delta)) {
   426           firstScroll = false;
   427           return true;
   428         }
   430         // TODO The current code looks for possible scrolling regions only if
   431         // this is the first scroll action but this should be more dynamic.
   432         if (!firstScroll) {
   433           return false;
   434         }
   436         current = ContentPanning._findPannable(targetParent(current));
   437       }
   439       // There is nothing scrollable here.
   440       return false;
   441     }
   442     return scroll;
   443   },
   445   get _domUtils() {
   446     delete this._domUtils;
   447     return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1']
   448                               .getService(Ci.inIDOMUtils);
   449   },
   451   get _activationTimer() {
   452     delete this._activationTimer;
   453     return this._activationTimer = Cc["@mozilla.org/timer;1"]
   454                                      .createInstance(Ci.nsITimer);
   455   },
   457   get _activationDelayMs() {
   458     let delay = Services.prefs.getIntPref('ui.touch_activation.delay_ms');
   459     delete this._activationDelayMs;
   460     return this._activationDelayMs = delay;
   461   },
   463   _resetActive: function cp_resetActive() {
   464     let elt = this.pointerDownTarget || this.target;
   465     let root = elt.ownerDocument || elt.document;
   466     this._setActive(root.documentElement);
   467   },
   469   _resetHover: function cp_resetHover() {
   470     const kStateHover = 0x00000004;
   471     try {
   472       let element = content.document.createElement('foo');
   473       this._domUtils.setContentState(element, kStateHover);
   474     } catch(e) {}
   475   },
   477   _setActive: function cp_setActive(elt) {
   478     const kStateActive = 0x00000001;
   479     this._domUtils.setContentState(elt, kStateActive);
   480   },
   482   _recvViewportChange: function(data) {
   483     let metrics = data.json;
   484     this._viewport = new Rect(metrics.x, metrics.y,
   485                               metrics.viewport.width,
   486                               metrics.viewport.height);
   487     this._cssCompositedRect = new Rect(metrics.x, metrics.y,
   488                                        metrics.cssCompositedRect.width,
   489                                        metrics.cssCompositedRect.height);
   490     this._cssPageRect = new Rect(metrics.cssPageRect.x,
   491                                  metrics.cssPageRect.y,
   492                                  metrics.cssPageRect.width,
   493                                  metrics.cssPageRect.height);
   494   },
   496   _recvDoubleTap: function(data) {
   497     let data = data.json;
   499     // We haven't received a metrics update yet; don't do anything.
   500     if (this._viewport == null) {
   501       return;
   502     }
   504     let win = content;
   506     let element = ElementTouchHelper.anyElementFromPoint(win, data.x, data.y);
   507     if (!element) {
   508       this._zoomOut();
   509       return;
   510     }
   512     while (element && !this._shouldZoomToElement(element))
   513       element = element.parentNode;
   515     if (!element) {
   516       this._zoomOut();
   517     } else {
   518       const margin = 15;
   519       let rect = ElementTouchHelper.getBoundingContentRect(element);
   521       let cssPageRect = this._cssPageRect;
   522       let viewport = this._viewport;
   523       let bRect = new Rect(Math.max(cssPageRect.x, rect.x - margin),
   524                            rect.y,
   525                            rect.w + 2 * margin,
   526                            rect.h);
   527       // constrict the rect to the screen's right edge
   528       bRect.width = Math.min(bRect.width, cssPageRect.right - bRect.x);
   530       // if the rect is already taking up most of the visible area and is stretching the
   531       // width of the page, then we want to zoom out instead.
   532       if (this._isRectZoomedIn(bRect, this._cssCompositedRect)) {
   533         this._zoomOut();
   534         return;
   535       }
   537       rect.x = Math.round(bRect.x);
   538       rect.y = Math.round(bRect.y);
   539       rect.w = Math.round(bRect.width);
   540       rect.h = Math.round(bRect.height);
   542       // if the block we're zooming to is really tall, and the user double-tapped
   543       // more than a screenful of height from the top of it, then adjust the y-coordinate
   544       // so that we center the actual point the user double-tapped upon. this prevents
   545       // flying to the top of a page when double-tapping to zoom in (bug 761721).
   546       // the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal
   547       // margins but not vertical ones.
   548       let cssTapY = viewport.y + data.y;
   549       if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) {
   550         rect.y = cssTapY - (rect.h / 2);
   551       }
   553       Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect));
   554     }
   555   },
   557   _handleVisibilityChange: function(evt) {
   558     if (!evt.target.hidden)
   559       return;
   561     this._resetHover();
   562   },
   564   _shouldZoomToElement: function(aElement) {
   565     let win = aElement.ownerDocument.defaultView;
   566     if (win.getComputedStyle(aElement, null).display == "inline")
   567       return false;
   568     if (aElement instanceof Ci.nsIDOMHTMLLIElement)
   569       return false;
   570     if (aElement instanceof Ci.nsIDOMHTMLQuoteElement)
   571       return false;
   572     return true;
   573   },
   575   _zoomOut: function() {
   576     let rect = new Rect(0, 0, 0, 0);
   577     Services.obs.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect));
   578   },
   580   _isRectZoomedIn: function(aRect, aViewport) {
   581     // This function checks to see if the area of the rect visible in the
   582     // viewport (i.e. the "overlapArea" variable below) is approximately 
   583     // the max area of the rect we can show.
   584     let vRect = new Rect(aViewport.x, aViewport.y, aViewport.width, aViewport.height);
   585     let overlap = vRect.intersect(aRect);
   586     let overlapArea = overlap.width * overlap.height;
   587     let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height);
   588     let showing = overlapArea / (aRect.width * availHeight);
   589     let ratioW = (aRect.width / vRect.width);
   590     let ratioH = (aRect.height / vRect.height);
   592     return (showing > 0.9 && (ratioW > 0.9 || ratioH > 0.9)); 
   593   },
   595   _finishPanning: function() {
   596     this.dragging = false;
   597     delete this.primaryPointerId;
   598     this._activationTimer.cancel();
   600     // If there is a scroll action, let's do a manual kinetic panning action.
   601     if (this.panning) {
   602       KineticPanning.start(this);
   603     }
   604   },
   606   _unloadHandler: function() {
   607     kObservedEvents.forEach((topic) => {
   608       Services.obs.removeObserver(this, topic);
   609     });
   610   }
   611 };
   613 // Min/max velocity of kinetic panning. This is in pixels/millisecond.
   614 const kMinVelocity = 0.2;
   615 const kMaxVelocity = 6;
   617 // Constants that affect the "friction" of the scroll pane.
   618 const kExponentialC = 1000;
   619 const kPolynomialC = 100 / 1000000;
   621 // How often do we change the position of the scroll pane?
   622 // Too often and panning may jerk near the end.
   623 // Too little and panning will be choppy. In milliseconds.
   624 const kUpdateInterval = 16;
   626 // The numbers of momentums to use for calculating the velocity of the pan.
   627 // Those are taken from the end of the action
   628 const kSamples = 5;
   630 const KineticPanning = {
   631   _position: new Point(0, 0),
   632   _velocity: new Point(0, 0),
   633   _acceleration: new Point(0, 0),
   635   get active() {
   636     return this.target !== null;
   637   },
   639   target: null,
   640   start: function kp_start(target) {
   641     this.target = target;
   643     // Calculate the initial velocity of the movement based on user input
   644     let momentums = this.momentums;
   645     let flick = momentums[momentums.length - 1].time - momentums[0].time < 300;
   647     let distance = new Point(0, 0);
   648     momentums.forEach(function(momentum) {
   649       distance.add(momentum.dx, momentum.dy);
   650     });
   652     function clampFromZero(x, min, max) {
   653       if (x >= 0)
   654         return Math.max(min, Math.min(max, x));
   655       return Math.min(-min, Math.max(-max, x));
   656     }
   658     let elapsed = momentums[momentums.length - 1].time - momentums[0].time;
   659     let velocityX = clampFromZero(distance.x / elapsed, 0, kMaxVelocity);
   660     let velocityY = clampFromZero(distance.y / elapsed, 0, kMaxVelocity);
   662     let velocity = this._velocity;
   663     if (flick) {
   664       // Very fast pan action that does not generate a click are very often pan
   665       // action. If this is a small gesture then it will not move the view a lot
   666       // and so it will be above the minimun threshold and not generate any
   667       // kinetic panning. This does not look on a device since this is often
   668       // a real gesture, so let's lower the velocity threshold for such moves.
   669       velocity.set(velocityX, velocityY);
   670     } else {
   671       velocity.set(Math.abs(velocityX) < kMinVelocity ? 0 : velocityX,
   672                    Math.abs(velocityY) < kMinVelocity ? 0 : velocityY);
   673     }
   674     this.momentums = [];
   676     // Set acceleration vector to opposite signs of velocity
   677     function sign(x) {
   678       return x ? (x > 0 ? 1 : -1) : 0;
   679     }
   681     this._acceleration.set(velocity.clone().map(sign).scale(-kPolynomialC));
   683     // Reset the position
   684     this._position.set(0, 0);
   686     this._startAnimation();
   688     this.target.onKineticBegin();
   689   },
   691   stop: function kp_stop() {
   692     this.reset();
   694     if (!this.target)
   695       return;
   697     this.target.onKineticEnd();
   698     this.target = null;
   699   },
   701   reset: function kp_reset() {
   702     this.momentums = [];
   703     this.distance.set(0, 0);
   704   },
   706   momentums: [],
   707   record: function kp_record(delta, timestamp) {
   708     this.momentums.push({ 'time': this._getTime(timestamp),
   709                           'dx' : delta.x, 'dy' : delta.y });
   711     // We only need to keep kSamples in this.momentums.
   712     if (this.momentums.length > kSamples) {
   713       this.momentums.shift();
   714     }
   716     this.distance.add(delta.x, delta.y);
   717   },
   719   _getTime: function kp_getTime(time) {
   720     // Touch events generated by the platform or hand-made are defined in
   721     // microseconds instead of milliseconds. Bug 77992 will fix this at the
   722     // platform level.
   723     if (time > Date.now()) {
   724       return Math.floor(time / 1000);
   725     } else {
   726       return time;
   727     }
   728   },
   730   get threshold() {
   731     let dpi = content.QueryInterface(Ci.nsIInterfaceRequestor)
   732                      .getInterface(Ci.nsIDOMWindowUtils)
   733                      .displayDPI;
   735     let threshold = Services.prefs.getIntPref('ui.dragThresholdX') / 240 * dpi;
   737     delete this.threshold;
   738     return this.threshold = threshold;
   739   },
   741   distance: new Point(0, 0),
   742   isPan: function cp_isPan() {
   743     return (Math.abs(this.distance.x) > this.threshold ||
   744             Math.abs(this.distance.y) > this.threshold);
   745   },
   747   _startAnimation: function kp_startAnimation() {
   748     let c = kExponentialC;
   749     function getNextPosition(position, v, a, t) {
   750       // Important traits for this function:
   751       //   p(t=0) is 0
   752       //   p'(t=0) is v0
   753       //
   754       // We use exponential to get a smoother stop, but by itself exponential
   755       // is too smooth at the end. Adding a polynomial with the appropriate
   756       // weight helps to balance
   757       position.set(v.x * Math.exp(-t / c) * -c + a.x * t * t + v.x * c,
   758                    v.y * Math.exp(-t / c) * -c + a.y * t * t + v.y * c);
   759     }
   761     let startTime = content.mozAnimationStartTime;
   762     let elapsedTime = 0, targetedTime = 0, averageTime = 0;
   764     let velocity = this._velocity;
   765     let acceleration = this._acceleration;
   767     let position = this._position;
   768     let nextPosition = new Point(0, 0);
   769     let delta = new Point(0, 0);
   771     let callback = (function(timestamp) {
   772       if (!this.target)
   773         return;
   775       // To make animation end fast enough but to keep smoothness, average the
   776       // ideal time frame (smooth animation) with the actual time lapse
   777       // (end fast enough).
   778       // Animation will never take longer than 2 times the ideal length of time.
   779       elapsedTime = timestamp - startTime;
   780       targetedTime += kUpdateInterval;
   781       averageTime = (targetedTime + elapsedTime) / 2;
   783       // Calculate new position.
   784       getNextPosition(nextPosition, velocity, acceleration, averageTime);
   785       delta.set(Math.round(nextPosition.x - position.x),
   786                 Math.round(nextPosition.y - position.y));
   788       // Test to see if movement is finished for each component.
   789       if (delta.x * acceleration.x > 0)
   790         delta.x = position.x = velocity.x = acceleration.x = 0;
   792       if (delta.y * acceleration.y > 0)
   793         delta.y = position.y = velocity.y = acceleration.y = 0;
   795       if (velocity.equals(0, 0) || delta.equals(0, 0)) {
   796         this.stop();
   797         return;
   798       }
   800       position.add(delta);
   801       if (this.target.onKineticPan(delta.scale(-1))) {
   802         this.stop();
   803         return;
   804       }
   806       content.mozRequestAnimationFrame(callback);
   807     }).bind(this);
   809     content.mozRequestAnimationFrame(callback);
   810   }
   811 };
   813 const ElementTouchHelper = {
   814   anyElementFromPoint: function(aWindow, aX, aY) {
   815     let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
   816     let elem = cwu.elementFromPoint(aX, aY, true, true);
   818     let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
   819     let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
   820     while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
   821       let rect = elem.getBoundingClientRect();
   822       aX -= rect.left;
   823       aY -= rect.top;
   824       cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
   825       elem = cwu.elementFromPoint(aX, aY, true, true);
   826     }
   828     return elem;
   829   },
   831   getBoundingContentRect: function(aElement) {
   832     if (!aElement)
   833       return {x: 0, y: 0, w: 0, h: 0};
   835     let document = aElement.ownerDocument;
   836     while (document.defaultView.frameElement)
   837       document = document.defaultView.frameElement.ownerDocument;
   839     let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
   840     let scrollX = {}, scrollY = {};
   841     cwu.getScrollXY(false, scrollX, scrollY);
   843     let r = aElement.getBoundingClientRect();
   845     // step out of iframes and frames, offsetting scroll values
   846     for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
   847       // adjust client coordinates' origin to be top left of iframe viewport
   848       let rect = frame.frameElement.getBoundingClientRect();
   849       let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
   850       let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
   851       scrollX.value += rect.left + parseInt(left);
   852       scrollY.value += rect.top + parseInt(top);
   853     }
   855     return {x: r.left + scrollX.value,
   856             y: r.top + scrollY.value,
   857             w: r.width,
   858             h: r.height };
   859   }
   860 };

mercurial