gecko/mobile/chrome/content/BrowserView.js
2009-07-17 16:17:57 -07:00

735 lines
24 KiB
JavaScript

// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/*
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Mozilla Mobile Browser.
*
* The Initial Developer of the Original Code is
* Mozilla Corporation.
* Portions created by the Initial Developer are Copyright (C) 2008
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Roy Frostig <rfrostig@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
let Ci = Components.interfaces;
// --- REMOVE ---
let noop = function() {};
let endl = '\n';
// --------------
function BrowserView(container, visibleRect) {
bindAll(this);
this.init(container, visibleRect);
}
/**
* A BrowserView maintains state of the viewport (browser, zoom level,
* dimensions) and the visible rectangle into the viewport, for every
* browser it is given (cf setBrowser()). In updates to the viewport state,
* a BrowserView (using its TileManager) renders parts of the page quasi-
* intelligently, with guarantees of having rendered and appended all of the
* visible browser content (aka the "critical rectangle").
*
* State is characterized in large part by two rectangles (and an implicit third):
* - Viewport: Always rooted at the origin, ie with (left, top) at (0, 0). The
* width and height (right and bottom) of this rectangle are that of the
* current viewport, which corresponds more or less to the transformed
* browser content (scaled by zoom level).
* - Visible: Corresponds to the client's viewing rectangle in viewport
* coordinates. Has (top, left) corresponding to position, and width & height
* corresponding to the clients viewing dimensions. Take note that the top
* and left of the visible rect are per-browser state, but that the width
* and height persist across setBrowser() calls. This is best explained by
* a simple example: user views browser A, pans to position (x0, y0), switches
* to browser B, where she finds herself at position (x1, y1), tilts her
* device so that visible rectangle's width and height change, and switches
* back to browser A. She expects to come back to position (x0, y0), but her
* device remains tilted.
* - Critical (the implicit one): The critical rectangle is the (possibly null)
* intersection of the visible and viewport rectangles. That is, it is that
* region of the viewport which is visible to the user. We care about this
* because it tells us which region must be rendered as soon as it is dirtied.
* The critical rectangle is mostly state that we do not keep in BrowserView
* but that our TileManager maintains.
*
* Example rectangle state configurations:
*
*
* +-------------------------------+
* |A |
* | |
* | |
* | |
* | +----------------+ |
* | |B,C | |
* | | | |
* | | | |
* | | | |
* | +----------------+ |
* | |
* | |
* | |
* | |
* | |
* +-------------------------------+
*
*
* A = viewport ; at (0, 0)
* B = visible ; at (x, y) where x > 0, y > 0
* C = critical ; at (x, y)
*
*
*
* +-------------------------------+
* |A |
* | |
* | |
* | |
* +----+-----------+ |
* |B .C | |
* | . | |
* | . | |
* | . | |
* +----+-----------+ |
* | |
* | |
* | |
* | |
* | |
* +-------------------------------+
*
*
* A = viewport ; at (0, 0)
* B = visible ; at (x, y) where x < 0, y > 0
* C = critical ; at (0, y)
*
*
* Maintaining per-browser state is a little bit of a hack involving attaching
* an object as the obfuscated dynamic JS property of the browser object, that
* hopefully no one but us will touch. See getViewportStateFromBrowser() for
* the property name.
*/
BrowserView.prototype = (
function() {
// -----------------------------------------------------------
// Privates
//
const kZoomLevelMin = 0.2;
const kZoomLevelMax = 4.0;
const kZoomLevelPrecision = 10000;
function visibleRectToCriticalRect(visibleRect, browserViewportState) {
return visibleRect.intersect(browserViewportState.viewportRect);
}
function clampZoomLevel(zl) {
let bounded = Math.min(Math.max(kZoomLevelMin, zl), kZoomLevelMax);
return Math.round(bounded * kZoomLevelPrecision) / kZoomLevelPrecision;
}
function pageZoomLevel(visibleRect, browserW, browserH) {
return clampZoomLevel(visibleRect.width / browserW);
}
function seenBrowser(browser) {
return !!(browser.__BrowserView__vps);
}
function initBrowserState(browser, visibleRect) {
let [browserW, browserH] = getBrowserDimensions(browser);
let zoomLevel = pageZoomLevel(visibleRect, browserW, browserH);
let viewportRect = (new wsRect(0, 0, browserW, browserH)).scale(zoomLevel, zoomLevel);
dump('--- initing browser to ---' + endl);
browser.__BrowserView__vps = new BrowserView.BrowserViewportState(viewportRect,
visibleRect.x,
visibleRect.y,
zoomLevel);
dump(browser.__BrowserView__vps.toString() + endl);
dump('--------------------------' + endl);
}
function getViewportStateFromBrowser(browser) {
return browser.__BrowserView__vps;
}
function getBrowserDimensions(browser) {
let cdoc = browser.contentDocument;
// These might not exist yet depending on page load state
let body = cdoc.body || {};
let html = cdoc.documentElement || {};
let w = Math.max(body.scrollWidth || 0, html.scrollWidth);
let h = Math.max(body.scrollHeight || 0, html.scrollHeight);
return [w, h];
}
function getContentScrollValues(browser) {
let cwu = getBrowserDOMWindowUtils(browser);
let scrollX = {};
let scrollY = {};
cwu.getScrollXY(false, scrollX, scrollY);
return [scrollX.value, scrollY.value];
}
function getBrowserDOMWindowUtils(browser) {
return browser.contentWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
}
function getNewBatchOperationState() {
return {
viewportSizeChanged: false,
dirtyAll: false
};
}
function clampViewportWH(width, height, visibleRect) {
let minW = visibleRect.width;
let minH = visibleRect.height;
return [Math.max(width, minW), Math.max(height, minH)];
}
function initContainer(container, visibleRect) {
container.style.width = visibleRect.width + 'px';
container.style.height = visibleRect.height + 'px';
container.style.overflow = '-moz-hidden-unscrollable';
}
function resizeContainerToViewport(container, viewportRect) {
container.style.width = viewportRect.width + 'px';
container.style.height = viewportRect.height + 'px';
}
// --- Change of coordinates functions --- //
// The following returned object becomes BrowserView.prototype
return {
// -----------------------------------------------------------
// Public instance methods
//
init: function init(container, visibleRect) {
this._batchOps = [];
this._container = container;
this._browserViewportState = null;
this._renderMode = 0;
this._tileManager = new TileManager(this._appendTile, this._removeTile, this);
this.setVisibleRect(visibleRect);
// !!! --- RESIZE HACK BEGIN -----
// remove this eventually
this._resizeHack = {
maxSeenW: 0,
maxSeenH: 0
};
// !!! --- RESIZE HACK END -------
},
setVisibleRect: function setVisibleRect(r) {
let bvs = this._browserViewportState;
let vr = this._visibleRect;
if (!vr)
this._visibleRect = vr = r.clone();
else
vr.copyFrom(r);
if (bvs) {
bvs.visibleX = vr.left;
bvs.visibleY = vr.top;
// reclamp minimally to the new visible rect
//this.setViewportDimensions(bvs.viewportRect.right, bvs.viewportRect.bottom);
} else
this._viewportChanged(false, false);
},
getVisibleRect: function getVisibleRect() {
return this._visibleRect.clone();
},
getVisibleRectX: function getVisibleRectX() { return this._visibleRect.x; },
getVisibleRectY: function getVisibleRectY() { return this._visibleRect.y; },
getVisibleRectWidth: function getVisibleRectWidth() { return this._visibleRect.width; },
getVisibleRectHeight: function getVisibleRectHeight() { return this._visibleRect.height; },
setViewportDimensions: function setViewportDimensions(width, height, causedByZoom) {
let bvs = this._browserViewportState;
let vis = this._visibleRect;
if (!bvs)
return;
//[width, height] = clampViewportWH(width, height, vis);
bvs.viewportRect.right = width;
bvs.viewportRect.bottom = height;
// XXX we might not want the user's page to disappear from under them
// at this point, which could happen if the container gets resized such
// that visible rect becomes entirely outside of viewport rect. might
// be wise to define what UX should be in this case, like a move occurs.
// then again, we could also argue this is the responsibility of the
// caller who would do such a thing...
this._viewportChanged(true, !!causedByZoom);
},
setZoomLevel: function setZoomLevel(zl) {
let bvs = this._browserViewportState;
if (!bvs)
return;
let newZL = clampZoomLevel(zl);
if (newZL != bvs.zoomLevel) {
let browserW = this.viewportToBrowser(bvs.viewportRect.right);
let browserH = this.viewportToBrowser(bvs.viewportRect.bottom);
bvs.zoomLevel = newZL; // side-effect: now scale factor in transformations is newZL
this.setViewportDimensions(this.browserToViewport(browserW),
this.browserToViewport(browserH));
}
},
getZoomLevel: function getZoomLevel() {
let bvs = this._browserViewportState;
if (!bvs)
return undefined;
return bvs.zoomLevel;
},
beginBatchOperation: function beginBatchOperation() {
this._batchOps.push(getNewBatchOperationState());
this.pauseRendering();
},
commitBatchOperation: function commitBatchOperation() {
let bops = this._batchOps;
if (bops.length == 0)
return;
let opState = bops.pop();
this._viewportChanged(opState.viewportSizeChanged, opState.dirtyAll);
this.resumeRendering();
},
discardBatchOperation: function discardBatchOperation() {
let bops = this._batchOps;
bops.pop();
this.resumeRendering();
},
discardAllBatchOperations: function discardAllBatchOperations() {
let bops = this._batchOps;
while (bops.length > 0)
this.discardBatchOperation();
},
moveVisibleBy: function moveVisibleBy(dx, dy) {
let vr = this._visibleRect;
let vs = this._browserViewportState;
this.onBeforeVisibleMove(dx, dy);
this.onAfterVisibleMove(dx, dy);
},
moveVisibleTo: function moveVisibleTo(x, y) {
let visibleRect = this._visibleRect;
let dx = x - visibleRect.x;
let dy = y - visibleRect.y;
this.moveBy(dx, dy);
},
/**
* Calls to this function need to be one-to-one with calls to
* resumeRendering()
*/
pauseRendering: function pauseRendering() {
this._renderMode++;
},
/**
* Calls to this function need to be one-to-one with calls to
* pauseRendering()
*/
resumeRendering: function resumeRendering(renderNow) {
if (this._renderMode > 0)
this._renderMode--;
if (renderNow || this._renderMode == 0)
this._tileManager.criticalRectPaint();
},
isRendering: function isRendering() {
return (this._renderMode == 0);
},
/**
* @param dx Guess delta to destination x coordinate
* @param dy Guess delta to destination y coordinate
*/
onBeforeVisibleMove: function onBeforeVisibleMove(dx, dy) {
let vs = this._browserViewportState;
let vr = this._visibleRect;
let destCR = visibleRectToCriticalRect(vr.clone().translate(dx, dy), vs);
this._tileManager.beginCriticalMove(destCR);
},
/**
* @param dx Actual delta to destination x coordinate
* @param dy Actual delta to destination y coordinate
*/
onAfterVisibleMove: function onAfterVisibleMove(dx, dy) {
let vs = this._browserViewportState;
let vr = this._visibleRect;
vr.translate(dx, dy);
vs.visibleX = vr.left;
vs.visibleY = vr.top;
let cr = visibleRectToCriticalRect(vr, vs);
this._tileManager.endCriticalMove(cr, this.isRendering());
},
setBrowser: function setBrowser(browser, doZoom) {
let currentBrowser = this._browser;
let browserChanged = (currentBrowser !== browser);
if (currentBrowser) {
currentBrowser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
// !!! --- RESIZE HACK BEGIN -----
// change to the real event type and perhaps refactor the handler function name
currentBrowser.removeEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
// !!! --- RESIZE HACK END -------
this.discardAllBatchOperations();
currentBrowser.setAttribute("type", "content");
currentBrowser.docShell.isOffScreenBrowser = false;
}
this._restoreBrowser(browser);
browser.setAttribute("type", "content-primary");
this.beginBatchOperation();
browser.addEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
// !!! --- RESIZE HACK BEGIN -----
// change to the real event type and perhaps refactor the handler function name
browser.addEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
// !!! --- RESIZE HACK END -------
if (doZoom) {
browser.docShell.isOffScreenBrowser = true;
this.zoomToPage();
}
this._viewportChanged(browserChanged, browserChanged);
this.commitBatchOperation();
},
handleMozAfterPaint: function handleMozAfterPaint(ev) {
let browser = this._browser;
let tm = this._tileManager;
let vs = this._browserViewportState;
let [scrollX, scrollY] = getContentScrollValues(browser);
let clientRects = ev.clientRects;
// !!! --- RESIZE HACK BEGIN -----
// remove this, cf explanation in loop below
let hack = this._resizeHack;
let hackSizeChanged = false;
// !!! --- RESIZE HACK END -------
let rects = [];
// loop backwards to avoid xpconnect penalty for .length
for (let i = clientRects.length - 1; i >= 0; --i) {
let e = clientRects.item(i);
let r = new wsRect(e.left + scrollX,
e.top + scrollY,
e.width, e.height);
this.browserToViewportRect(r);
r.round();
if (r.right < 0 || r.bottom < 0)
continue;
// !!! --- RESIZE HACK BEGIN -----
// remove this. this is where we make 'lazy' calculations
// that hint at a browser size change and fake the size change
// event dispach
if (r.right > hack.maxSeenW) {
hack.maxSeenW = r.right;
hackSizeChanged = true;
}
if (r.bottom > hack.maxSeenH) {
hack.maxSeenH = r.bottom;
hackSizeChanged = true;
}
// !!! --- RESIZE HACK END -------
try {
r.restrictTo(vs.viewportRect);
rects.push(r);
} catch(ex) { dump("fail:(\n"); }
}
tm.dirtyRects(rects, this.isRendering());
},
// !!! --- RESIZE HACK BEGIN -----
simulateMozAfterSizeChange: function simulateMozAfterSizeChange(width, height) {
let [w, h] = getBrowserDimensions(this._browser);
let ev = document.createEvent("MouseEvents");
ev.initMouseEvent("FakeMozAfterSizeChange", false, false, window, 0, w, h, 0, 0, false, false, false, false, 0, null);
this._browser.dispatchEvent(ev);
},
// !!! --- RESIZE HACK END -------
handleMozAfterSizeChange: function handleMozAfterSizeChange(ev) {
dump("win\n");
// !!! --- RESIZE HACK BEGIN -----
// get the correct properties off of the event, these are wrong because
// we're using a MouseEvent since it has an X and Y prop of some sort and
// we piggyback on that.
let w = ev.screenX;
let h = ev.screenY;
// !!! --- RESIZE HACK END -------
debugger;
this.setViewportDimensions(this.browserToViewport(w), this.browserToViewport(h));
},
zoomToPage: function zoomToPage() {
let browser = this._browser;
if (!browser)
return;
let [w, h] = getBrowserDimensions(browser);
this.setZoomLevel(pageZoomLevel(this._visibleRect, w, h));
},
zoom: function zoom(aDirection) {
if (aDirection == 0)
return;
var zoomDelta = 0.05; // 1/20
if (aDirection >= 0)
zoomDelta *= -1;
this.zoomLevel = this._zoomLevel + zoomDelta;
},
viewportToBrowser: function viewportToBrowser(x) {
let bvs = this._browserViewportState;
if (!bvs)
throw "No browser is set";
return x / bvs.zoomLevel;
},
browserToViewport: function browserToViewport(x) {
let bvs = this._browserViewportState;
if (!bvs)
throw "No browser is set";
return x * bvs.zoomLevel;
},
viewportToBrowserRect: function viewportToBrowserRect(rect) {
let f = this.viewportToBrowser(1.0);
return rect.scale(f, f);
},
browserToViewportRect: function browserToViewportRect(rect) {
let f = this.browserToViewport(1.0);
return rect.scale(f, f);
},
browserToViewportCanvasContext: function browserToViewportCanvasContext(ctx) {
let f = this.browserToViewport(1.0);
ctx.scale(f, f);
},
// -----------------------------------------------------------
// Private instance methods
//
_restoreBrowser: function _restoreBrowser(browser) {
let vr = this._visibleRect;
if (!seenBrowser(browser))
initBrowserState(browser, vr);
let bvs = getViewportStateFromBrowser(browser);
this._contentWindow = browser.contentWindow;
this._browser = browser;
this._browserViewportState = bvs;
vr.left = bvs.visibleX;
vr.top = bvs.visibleY;
this._tileManager.setBrowser(browser);
},
_viewportChanged: function _viewportChanged(viewportSizeChanged, dirtyAll) {
let bops = this._batchOps;
if (bops.length > 0) {
let opState = bops[bops.length - 1];
if (viewportSizeChanged)
opState.viewportSizeChanged = viewportSizeChanged;
if (dirtyAll)
opState.dirtyAll = dirtyAll;
return;
}
let bvs = this._browserViewportState;
let vis = this._visibleRect;
// !!! --- RESIZE HACK BEGIN -----
// We want to uncomment this for perf, but we can't with the hack in place
// because the mozAfterPaint gives us rects that we use to create the
// fake mozAfterResize event, so we can't just clear things.
/*
if (dirtyAll) {
// We're about to mark the entire viewport dirty, so we can clear any
// queued afterPaint events that will cause redundant draws
getBrowserDOMWindowUtils(this._browser).clearMozAfterPaintEvents();
}
*/
// !!! --- RESIZE HACK END -------
if (bvs) {
resizeContainerToViewport(this._container, bvs.viewportRect);
this._tileManager.viewportChangeHandler(bvs.viewportRect,
visibleRectToCriticalRect(vis, bvs),
viewportSizeChanged,
dirtyAll);
}
},
_appendTile: function _appendTile(tile) {
let canvas = tile.getContentImage();
canvas.style.position = "absolute";
canvas.style.left = tile.x + "px";
canvas.style.top = tile.y + "px";
// XXX better for the jit
//canvas.setAttribute("style", "position: absolute; left: " + tile.boundRect.left + "px; " + "top: " + tile.boundRect.top + "px;");
this._container.appendChild(canvas);
//dump('++ ' + tile.toString(true) + endl);
},
_removeTile: function _removeTile(tile) {
let canvas = tile.getContentImage();
this._container.removeChild(canvas);
//dump('-- ' + tile.toString(true) + endl);
}
};
}
)();
// -----------------------------------------------------------
// Helper structures
//
BrowserView.BrowserViewportState = function(viewportRect,
visibleX,
visibleY,
zoomLevel) {
this.init(viewportRect, visibleX, visibleY, zoomLevel);
};
BrowserView.BrowserViewportState.prototype = {
init: function init(viewportRect, visibleX, visibleY, zoomLevel) {
this.viewportRect = viewportRect;
this.visibleX = visibleX;
this.visibleY = visibleY;
this.zoomLevel = zoomLevel;
},
clone: function clone() {
return new BrowserView.BrowserViewportState(this.viewportRect,
this.visibleX,
this.visibleY,
this.zoomLevel);
},
toString: function toString() {
let props = ['\tviewportRect=' + this.viewportRect.toString(),
'\tvisibleX=' + this.visibleX,
'\tvisibleY=' + this.visibleY,
'\tzoomLevel=' + this.zoomLevel];
return '[BrowserViewportState] {\n' + props.join(',\n') + '\n}';
}
};