gecko/dom/browser-element/BrowserElementScrolling.js
Chris Jones ef35aa58e9 Bug 750977: Implement glue code for asynchronous panning/zooming. r=jlebar,roc,vingtetun
This is a rollup of three separate patches
 - Add nsIDocShell.asyncPanZoomEnabled. r=jlebar
 - Have BrowserElementChild service repaint requests and handle fallback synchronous scrolling (for now). r=jlebar,vingtetun
 - Glue async pan/zoom logic up between compositing, event dispatch, and repaint requests. r=roc

--HG--
rename : b2g/chrome/content/webapi.js => dom/browser-element/BrowserElementScrolling.js
2012-07-19 23:48:27 -07:00

382 lines
12 KiB
JavaScript

/* 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);
}
};