mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-30 05:35:31 +00:00
831e04c2e3
This is a rollup of the following patches - Change the interpretation of FrameMetrics.mZoom to a "resolution-indepedent zoom", instead of a resolution-depedent scale factor. r=roc - Remove mention of "meta" from TabChild. r=roc - Remove some useless logging. r=roc - Tag FrameMetrics with its composition bounds at paint time. r=roc - Add a helper to calculate the render resolution for a FrameMetrics. r=roc - Add a helper to compute the approximate CSS dimensions a FrameMetrics will cover during composition. r=roc - BrowserElementScrolling doesn't actually care about zoom or resolution. r=roc - Accept the viewport that content has calculated, when it's received the latest widget geometry update. r=roc - Mechanically separate uses of zoom/resolution based on new definitions. r=roc - Convert GetViewportInfo()'s resolution-dependent scale into resolution-indepedent zoom. r=roc - Reinterpret defaultZoom == 0.0 as "intrinsic scale". r=jwir3,roc
538 lines
18 KiB
JavaScript
538 lines
18 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, false);
|
|
});
|
|
|
|
addMessageListener("Viewport:Change", this._recvViewportChange.bind(this));
|
|
addMessageListener("Gesture:DoubleTap", this._recvDoubleTap.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 we found a target, that means we have found a scrollable subframe. In
|
|
// this case, and if we are using async panning and zooming on the parent
|
|
// frame, inform the pan/zoom controller that it should not attempt to
|
|
// handle any touch events it gets until the next batch (meaning the next
|
|
// time we get a touch end).
|
|
if (this.target != null && ContentPanning._asyncPanZoomForViewportFrame) {
|
|
var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
|
|
os.notifyObservers(docShell, 'cancel-default-pan-zoom', null);
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
evt.stopPropagation();
|
|
evt.preventDefault();
|
|
},
|
|
|
|
|
|
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);
|
|
|
|
let isScrollableTextarea = (node.tagName == 'TEXTAREA' &&
|
|
(node.scrollHeight > node.clientHeight ||
|
|
node.scrollWidth > node.clientWidth ||
|
|
('scrollLeftMax' in node && node.scrollLeftMax > 0) ||
|
|
('scrollTopMax' in node && node.scrollTopMax > 0)));
|
|
if (isScroll || isAuto || isScrollableTextarea)
|
|
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];
|
|
|
|
if (nodeContent.scrollMaxX || nodeContent.scrollMaxY) {
|
|
return [nodeContent, this._generateCallback(nodeContent)];
|
|
}
|
|
|
|
return [null, null];
|
|
},
|
|
|
|
_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 metrics = data.json;
|
|
this._viewport = new Rect(metrics.x, metrics.y,
|
|
metrics.viewport.width,
|
|
metrics.viewport.height);
|
|
this._cssPageRect = new Rect(metrics.cssPageRect.x,
|
|
metrics.cssPageRect.y,
|
|
metrics.cssPageRect.width,
|
|
metrics.cssPageRect.height);
|
|
},
|
|
|
|
_recvDoubleTap: function(data) {
|
|
let data = data.json;
|
|
|
|
// We haven't received a metrics update yet; don't do anything.
|
|
if (this._viewport == null) {
|
|
return;
|
|
}
|
|
|
|
let win = content;
|
|
|
|
let element = ElementTouchHelper.anyElementFromPoint(win, data.x, data.y);
|
|
if (!element) {
|
|
this._zoomOut();
|
|
return;
|
|
}
|
|
|
|
while (element && !this._shouldZoomToElement(element))
|
|
element = element.parentNode;
|
|
|
|
if (!element) {
|
|
this._zoomOut();
|
|
} else {
|
|
const margin = 15;
|
|
let rect = ElementTouchHelper.getBoundingContentRect(element);
|
|
|
|
let cssPageRect = this._cssPageRect;
|
|
let viewport = this._viewport;
|
|
let bRect = new Rect(Math.max(cssPageRect.x, rect.x - margin),
|
|
rect.y,
|
|
rect.w + 2 * margin,
|
|
rect.h);
|
|
// constrict the rect to the screen's right edge
|
|
bRect.width = Math.min(bRect.width, cssPageRect.right - bRect.x);
|
|
|
|
// if the rect is already taking up most of the visible area and is stretching the
|
|
// width of the page, then we want to zoom out instead.
|
|
if (this._isRectZoomedIn(bRect, viewport)) {
|
|
this._zoomOut();
|
|
return;
|
|
}
|
|
|
|
rect.x = Math.round(bRect.x);
|
|
rect.y = Math.round(bRect.y);
|
|
rect.w = Math.round(bRect.width);
|
|
rect.h = Math.round(Math.min(bRect.width * viewport.height / viewport.height, bRect.height));
|
|
|
|
// if the block we're zooming to is really tall, and the user double-tapped
|
|
// more than a screenful of height from the top of it, then adjust the y-coordinate
|
|
// so that we center the actual point the user double-tapped upon. this prevents
|
|
// flying to the top of a page when double-tapping to zoom in (bug 761721).
|
|
// the 1.2 multiplier is just a little fuzz to compensate for bRect including horizontal
|
|
// margins but not vertical ones.
|
|
let cssTapY = viewport.y + data.y;
|
|
if ((bRect.height > rect.h) && (cssTapY > rect.y + (rect.h * 1.2))) {
|
|
rect.y = cssTapY - (rect.h / 2);
|
|
}
|
|
|
|
var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
|
|
os.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect));
|
|
}
|
|
},
|
|
|
|
_shouldZoomToElement: function(aElement) {
|
|
let win = aElement.ownerDocument.defaultView;
|
|
if (win.getComputedStyle(aElement, null).display == "inline")
|
|
return false;
|
|
if (aElement instanceof Ci.nsIDOMHTMLLIElement)
|
|
return false;
|
|
if (aElement instanceof Ci.nsIDOMHTMLQuoteElement)
|
|
return false;
|
|
return true;
|
|
},
|
|
|
|
_zoomOut: function() {
|
|
let rect = new Rect(0, 0, 0, 0);
|
|
var os = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
|
|
os.notifyObservers(docShell, 'browser-zoom-to-rect', JSON.stringify(rect));
|
|
},
|
|
|
|
_isRectZoomedIn: function(aRect, aViewport) {
|
|
// This function checks to see if the area of the rect visible in the
|
|
// viewport (i.e. the "overlapArea" variable below) is approximately
|
|
// the max area of the rect we can show. It also checks that the rect
|
|
// is actually on-screen by testing the left and right edges of the rect.
|
|
// In effect, this tells us whether or not zooming in to this rect
|
|
// will significantly change what the user is seeing.
|
|
const minDifference = -20;
|
|
const maxDifference = 20;
|
|
|
|
let vRect = new Rect(aViewport.x, aViewport.y, aViewport.width, aViewport.height);
|
|
let overlap = vRect.intersect(aRect);
|
|
let overlapArea = overlap.width * overlap.height;
|
|
let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height);
|
|
let showing = overlapArea / (aRect.width * availHeight);
|
|
let dw = (aRect.width - vRect.width);
|
|
let dx = (aRect.x - vRect.x);
|
|
|
|
return (showing > 0.9 &&
|
|
dx > minDifference && dx < maxDifference &&
|
|
dw > minDifference && dw < maxDifference);
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
const ElementTouchHelper = {
|
|
anyElementFromPoint: function(aWindow, aX, aY) {
|
|
let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
|
let elem = cwu.elementFromPoint(aX, aY, true, true);
|
|
|
|
let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
|
|
let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
|
|
while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
|
|
let rect = elem.getBoundingClientRect();
|
|
aX -= rect.left;
|
|
aY -= rect.top;
|
|
cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
|
elem = cwu.elementFromPoint(aX, aY, true, true);
|
|
}
|
|
|
|
return elem;
|
|
},
|
|
|
|
getBoundingContentRect: function(aElement) {
|
|
if (!aElement)
|
|
return {x: 0, y: 0, w: 0, h: 0};
|
|
|
|
let document = aElement.ownerDocument;
|
|
while (document.defaultView.frameElement)
|
|
document = document.defaultView.frameElement.ownerDocument;
|
|
|
|
let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
|
|
let scrollX = {}, scrollY = {};
|
|
cwu.getScrollXY(false, scrollX, scrollY);
|
|
|
|
let r = aElement.getBoundingClientRect();
|
|
|
|
// step out of iframes and frames, offsetting scroll values
|
|
for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
|
|
// adjust client coordinates' origin to be top left of iframe viewport
|
|
let rect = frame.frameElement.getBoundingClientRect();
|
|
let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
|
|
let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
|
|
scrollX.value += rect.left + parseInt(left);
|
|
scrollY.value += rect.top + parseInt(top);
|
|
}
|
|
|
|
return {x: r.left + scrollX.value,
|
|
y: r.top + scrollY.value,
|
|
w: r.width,
|
|
h: r.height };
|
|
}
|
|
};
|