Wed, 31 Dec 2014 06:55:50 +0100
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 };