/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ const ContentPanning = { init: function cp_init() { ['mousedown', 'mouseup', 'mousemove'].forEach(function(type) { addEventListener(type, ContentPanning, true); }); addMessageListener("Viewport:Change", this._recvViewportChange.bind(this)); }, handleEvent: function cp_handleEvent(evt) { switch (evt.type) { case 'mousedown': this.onTouchStart(evt); break; case 'mousemove': this.onTouchMove(evt); break; case 'mouseup': this.onTouchEnd(evt); break; case 'click': evt.stopPropagation(); evt.preventDefault(); let target = evt.target; let view = target.ownerDocument ? target.ownerDocument.defaultView : target; view.removeEventListener('click', this, true, true); break; } }, position: new Point(0 , 0), onTouchStart: function cp_onTouchStart(evt) { this.dragging = true; this.panning = false; let oldTarget = this.target; [this.target, this.scrollCallback] = this.getPannable(evt.target); // If there is a pan animation running (from a previous pan gesture) and // the user touch back the screen, stop this animation immediatly and // prevent the possible click action if the touch happens on the same // target. this.preventNextClick = false; if (KineticPanning.active) { KineticPanning.stop(); if (oldTarget && oldTarget == this.target) this.preventNextClick = true; } this.position.set(evt.screenX, evt.screenY); KineticPanning.record(new Point(0, 0), evt.timeStamp); }, onTouchEnd: function cp_onTouchEnd(evt) { if (!this.dragging) return; this.dragging = false; this.onTouchMove(evt); let click = evt.detail; if (this.target && click && (this.panning || this.preventNextClick)) { let target = this.target; let view = target.ownerDocument ? target.ownerDocument.defaultView : target; view.addEventListener('click', this, true, true); } if (this.panning) KineticPanning.start(this); }, onTouchMove: function cp_onTouchMove(evt) { if (!this.dragging || !this.scrollCallback) return; let current = this.position; let delta = new Point(evt.screenX - current.x, evt.screenY - current.y); current.set(evt.screenX, evt.screenY); KineticPanning.record(delta, evt.timeStamp); this.scrollCallback(delta.scale(-1)); // If a pan action happens, cancel the active state of the // current target. if (!this.panning && KineticPanning.isPan()) { this.panning = true; this._resetActive(); } }, onKineticBegin: function cp_onKineticBegin(evt) { }, onKineticPan: function cp_onKineticPan(delta) { return !this.scrollCallback(delta); }, onKineticEnd: function cp_onKineticEnd() { if (!this.dragging) this.scrollCallback = null; }, getPannable: function cp_getPannable(node) { if (!(node instanceof Ci.nsIDOMHTMLElement) || node.tagName == 'HTML') return [null, null]; let nodeContent = node.ownerDocument.defaultView; while (!(node instanceof Ci.nsIDOMHTMLBodyElement)) { let style = nodeContent.getComputedStyle(node, null); let overflow = [style.getPropertyValue('overflow'), style.getPropertyValue('overflow-x'), style.getPropertyValue('overflow-y')]; let rect = node.getBoundingClientRect(); let isAuto = (overflow.indexOf('auto') != -1 && (rect.height < node.scrollHeight || rect.width < node.scrollWidth)); let isScroll = (overflow.indexOf('scroll') != -1); if (isScroll || isAuto) return [node, this._generateCallback(node)]; node = node.parentNode; } if (ContentPanning._asyncPanZoomForViewportFrame && nodeContent === content) // The parent context is asynchronously panning and zooming our // root scrollable frame, so don't use our synchronous fallback. return [null, null]; return [nodeContent, this._generateCallback(nodeContent)]; }, _generateCallback: function cp_generateCallback(content) { function scroll(delta) { if (content instanceof Ci.nsIDOMHTMLElement) { let oldX = content.scrollLeft, oldY = content.scrollTop; content.scrollLeft += delta.x; content.scrollTop += delta.y; let newX = content.scrollLeft, newY = content.scrollTop; return (newX != oldX) || (newY != oldY); } else { let oldX = content.scrollX, oldY = content.scrollY; content.scrollBy(delta.x, delta.y); let newX = content.scrollX, newY = content.scrollY; return (newX != oldX) || (newY != oldY); } } return scroll; }, get _domUtils() { delete this._domUtils; return this._domUtils = Cc['@mozilla.org/inspector/dom-utils;1'] .getService(Ci.inIDOMUtils); }, _resetActive: function cp_resetActive() { let root = this.target.ownerDocument || this.target.document; const kStateActive = 0x00000001; this._domUtils.setContentState(root.documentElement, kStateActive); }, get _asyncPanZoomForViewportFrame() { return docShell.asyncPanZoomEnabled; }, _recvViewportChange: function(data) { let viewport = data.json; let displayPort = viewport.displayPort; let screenWidth = viewport.screenSize.width; let screenHeight = viewport.screenSize.height; let x = viewport.x; let y = viewport.y; let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); cwu.setCSSViewport(screenWidth, screenHeight); // Set scroll position cwu.setScrollPositionClampingScrollPortSize( screenWidth / viewport.zoom, screenHeight / viewport.zoom); content.scrollTo(x, y); cwu.setResolution(displayPort.resolution, displayPort.resolution); let element = null; if (content.document && (element = content.document.documentElement)) { cwu.setDisplayPortForElement(displayPort.left, displayPort.top, displayPort.width, displayPort.height, element); } } }; ContentPanning.init(); // Min/max velocity of kinetic panning. This is in pixels/millisecond. const kMinVelocity = 0.4; const kMaxVelocity = 6; // Constants that affect the "friction" of the scroll pane. const kExponentialC = 1000; const kPolynomialC = 100 / 1000000; // How often do we change the position of the scroll pane? // Too often and panning may jerk near the end. // Too little and panning will be choppy. In milliseconds. const kUpdateInterval = 16; // The numbers of momentums to use for calculating the velocity of the pan. // Those are taken from the end of the action const kSamples = 5; const KineticPanning = { _position: new Point(0, 0), _velocity: new Point(0, 0), _acceleration: new Point(0, 0), get active() { return this.target !== null; }, target: null, start: function kp_start(target) { this.target = target; // Calculate the initial velocity of the movement based on user input let momentums = this.momentums.slice(-kSamples); let distance = new Point(0, 0); momentums.forEach(function(momentum) { distance.add(momentum.dx, momentum.dy); }); let elapsed = momentums[momentums.length - 1].time - momentums[0].time; function clampFromZero(x, min, max) { if (x >= 0) return Math.max(min, Math.min(max, x)); return Math.min(-min, Math.max(-max, x)); } let velocityX = clampFromZero(distance.x / elapsed, 0, kMaxVelocity); let velocityY = clampFromZero(distance.y / elapsed, 0, kMaxVelocity); let velocity = this._velocity; velocity.set(Math.abs(velocityX) < kMinVelocity ? 0 : velocityX, Math.abs(velocityY) < kMinVelocity ? 0 : velocityY); this.momentums = []; // Set acceleration vector to opposite signs of velocity function sign(x) { return x ? (x > 0 ? 1 : -1) : 0; } this._acceleration.set(velocity.clone().map(sign).scale(-kPolynomialC)); // Reset the position this._position.set(0, 0); this._startAnimation(); this.target.onKineticBegin(); }, stop: function kp_stop() { if (!this.target) return; this.momentums = []; this.distance.set(0, 0); this.target.onKineticEnd(); this.target = null; }, momentums: [], record: function kp_record(delta, timestamp) { this.momentums.push({ 'time': timestamp, 'dx' : delta.x, 'dy' : delta.y }); this.distance.add(delta.x, delta.y); }, get threshold() { let dpi = content.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils) .displayDPI; let threshold = Services.prefs.getIntPref('ui.dragThresholdX') / 240 * dpi; delete this.threshold; return this.threshold = threshold; }, distance: new Point(0, 0), isPan: function cp_isPan() { return (Math.abs(this.distance.x) > this.threshold || Math.abs(this.distance.y) > this.threshold); }, _startAnimation: function kp_startAnimation() { let c = kExponentialC; function getNextPosition(position, v, a, t) { // Important traits for this function: // p(t=0) is 0 // p'(t=0) is v0 // // We use exponential to get a smoother stop, but by itself exponential // is too smooth at the end. Adding a polynomial with the appropriate // weight helps to balance position.set(v.x * Math.exp(-t / c) * -c + a.x * t * t + v.x * c, v.y * Math.exp(-t / c) * -c + a.y * t * t + v.y * c); } let startTime = content.mozAnimationStartTime; let elapsedTime = 0, targetedTime = 0, averageTime = 0; let velocity = this._velocity; let acceleration = this._acceleration; let position = this._position; let nextPosition = new Point(0, 0); let delta = new Point(0, 0); let callback = (function(timestamp) { if (!this.target) return; // To make animation end fast enough but to keep smoothness, average the // ideal time frame (smooth animation) with the actual time lapse // (end fast enough). // Animation will never take longer than 2 times the ideal length of time. elapsedTime = timestamp - startTime; targetedTime += kUpdateInterval; averageTime = (targetedTime + elapsedTime) / 2; // Calculate new position. getNextPosition(nextPosition, velocity, acceleration, averageTime); delta.set(Math.round(nextPosition.x - position.x), Math.round(nextPosition.y - position.y)); // Test to see if movement is finished for each component. if (delta.x * acceleration.x > 0) delta.x = position.x = velocity.x = acceleration.x = 0; if (delta.y * acceleration.y > 0) delta.y = position.y = velocity.y = acceleration.y = 0; if (velocity.equals(0, 0) || delta.equals(0, 0)) { this.stop(); return; } position.add(delta); if (this.target.onKineticPan(delta.scale(-1))) { this.stop(); return; } content.mozRequestAnimationFrame(callback); }).bind(this); content.mozRequestAnimationFrame(callback); } };