[mq]: tileman

This commit is contained in:
Roy Frostig 2009-07-17 17:14:49 -07:00
parent 3de71cc1d1
commit 00ca6f826b
4 changed files with 2326 additions and 102 deletions

View File

@ -0,0 +1,748 @@
// -*- 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) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Roy Frostig <rfrostig@mozilla.com>
* Stuart Parmenter <stuart@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;
function BrowserView(container, visibleRect) {
Util.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 BrowserView.Util.getViewportStateFromBrowser()
* for the property name.
*/
const kBrowserViewZoomLevelMin = 0.2;
const kBrowserViewZoomLevelMax = 4.0;
const kBrowserViewZoomLevelPrecision = 10000;
// -----------------------------------------------------------
// Util/convenience functions.
//
// These are mostly for use by BrowserView itself, but if you find them handy anywhere
// else, feel free.
//
BrowserView.Util = {
visibleRectToCriticalRect: function visibleRectToCriticalRect(visibleRect, browserViewportState) {
return visibleRect.intersect(browserViewportState.viewportRect);
},
clampZoomLevel: function clampZoomLevel(zl) {
let bounded = Math.min(Math.max(kBrowserViewZoomLevelMin, zl), kBrowserViewZoomLevelMax);
return Math.round(bounded * kBrowserViewZoomLevelPrecision) / kBrowserViewZoomLevelPrecision;
},
pageZoomLevel: function pageZoomLevel(visibleRect, browserW, browserH) {
return BrowserView.Util.clampZoomLevel(visibleRect.width / browserW);
},
seenBrowser: function seenBrowser(browser) {
return !!(browser.__BrowserView__vps);
},
initBrowserState: function initBrowserState(browser, visibleRect) {
let [browserW, browserH] = BrowserView.Util.getBrowserDimensions(browser);
let zoomLevel = BrowserView.Util.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);
},
getViewportStateFromBrowser: function getViewportStateFromBrowser(browser) {
return browser.__BrowserView__vps;
},
getBrowserDimensions: 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];
},
getContentScrollValues: function getContentScrollValues(browser) {
let cwu = BrowserView.Util.getBrowserDOMWindowUtils(browser);
let scrollX = {};
let scrollY = {};
cwu.getScrollXY(false, scrollX, scrollY);
return [scrollX.value, scrollY.value];
},
getBrowserDOMWindowUtils: function getBrowserDOMWindowUtils(browser) {
return browser.contentWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
},
getNewBatchOperationState: function getNewBatchOperationState() {
return {
viewportSizeChanged: false,
dirtyAll: false
};
},
clampViewportWH: function clampViewportWH(width, height, visibleRect) {
let minW = visibleRect.width;
let minH = visibleRect.height;
return [Math.max(width, minW), Math.max(height, minH)];
},
initContainer: function initContainer(container, visibleRect) {
container.style.width = visibleRect.width + 'px';
container.style.height = visibleRect.height + 'px';
container.style.overflow = '-moz-hidden-unscrollable';
},
resizeContainerToViewport: function resizeContainerToViewport(container, viewportRect) {
container.style.width = viewportRect.width + 'px';
container.style.height = viewportRect.height + 'px';
}
};
BrowserView.prototype = {
// -----------------------------------------------------------
// Public instance methods
//
init: function init(container, visibleRect) {
this._batchOps = [];
this._container = container;
this._browser = null;
this._browserViewportState = null;
this._contentWindow = 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] = BrowserView.Util.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(BrowserView.Util.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 = BrowserView.Util.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 = BrowserView.Util.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);
if (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] = BrowserView.Util.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.maxW) {
hack.maxW = rect.right;
hackSizeChanged = true;
}
if (r.bottom > hack.maxH) {
hack.maxH = rect.bottom;
hackSizeChanged = true;
}
// !!! --- RESIZE HACK END -------
r.restrictTo(vs.viewportRect);
rects.push(r);
}
// !!! --- RESIZE HACK BEGIN -----
// remove this, cf explanation in loop above
if (hackSizeChanged)
this.simulateMozAfterSizeChange(browser, hack.maxW, hack.maxH);
// !!! --- RESIZE HACK END -------
tm.dirtyRects(rects, this.isRendering());
},
// !!! --- RESIZE HACK BEGIN -----
simulateMozAfterSizeChange: function simulateMozAfterSizeChange(browser, width, height) {
//let ev = document.createElement("MouseEvents");
//ev.initEvent("FakeMozAfterSizeChange", false, false, window, 0, width, height);
//browser.dispatchEvent(ev);
this.handleMozAfterSizeChange({screenX: width, screenY: height});
},
// !!! --- RESIZE HACK END -------
handleMozAfterSizeChange: function handleMozAfterPaint(ev) {
// !!! --- 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 -------
this.setViewportDimensions(w, h);
},
zoomToPage: function zoomToPage() {
let browser = this._browser;
if (!browser)
return;
let [w, h] = BrowserView.Util.getBrowserDimensions(browser);
this.setZoomLevel(BrowserView.Util.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 bvs = null;
if (browser) {
let vr = this._visibleRect;
if (!BrowserView.Util.seenBrowser(browser))
BrowserView.Util.initBrowserState(browser, vr);
bvs = BrowserView.Util.getViewportStateFromBrowser(browser);
vr.left = bvs.visibleX;
vr.top = bvs.visibleY;
}
this._browser = browser;
this._contentWindow = (browser) ? browser.contentWindow : null;
this._browserViewportState = bvs;
},
_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
BrowserView.Util.getBrowserDOMWindowUtils(this._browser).clearMozAfterPaintEvents();
}
*/
// !!! --- RESIZE HACK END -------
if (bvs) {
BrowserView.Util.resizeContainerToViewport(this._container, bvs.viewportRect);
this._tileManager.viewportChangeHandler(bvs.viewportRect,
BrowserView.Util.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 The above causes a trace abort, and this function is called back in the tight
// render-heavy loop in TileManager, so even though what we do below isn't so proper
// and takes longer on the Platform/C++ emd, it's better than causing a trace abort
// in our tight loop. :/
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
//
/**
* A BrowserViewportState maintains viewport state information that is unique to each
* browser. It does not hold *all* viewport state maintained by BrowserView. For
* instance, it does not maintain width and height of the visible rectangle (but it
* does keep the top and left coordinates (cf visibleX, visibleY)), since those are not
* characteristic of the current browser in view.
*/
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}';
}
};

File diff suppressed because it is too large Load Diff

View File

@ -95,25 +95,15 @@ function wsBorder(t, l, b, r) {
wsBorder.prototype = {
get left() { return this._l; },
get right() { return this._r; },
get top() { return this._t; },
get bottom() { return this._b; },
set left(v) { this._l = v; },
set right(v) { this._r = v; },
set top(v) { this._t = v; },
set bottom(v) { this._b = v; },
setBorder: function (t, l, b, r) {
this._t = t;
this._l = l;
this._b = b;
this._r = r;
this.top = t;
this.left = l;
this.bottom = b;
this.right = r;
},
toString: function () {
return "[l:" + this._l + ",t:" + this._t + ",r:" + this._r + ",b:" + this._b + "]";
return "[l:" + this.left + ",t:" + this.top + ",r:" + this.right + ",b:" + this.bottom + "]";
}
};
@ -123,93 +113,103 @@ wsBorder.prototype = {
* Rectangle class, with both x/y/w/h and t/l/b/r accessors.
*/
function wsRect(x, y, w, h) {
this.setRect(x, y, w, h);
this.left = x;
this.top = y;
this.right = x+w;
this.bottom = y+h;
}
wsRect.prototype = {
get x() { return this._l; },
get y() { return this._t; },
get width() { return this._r - this._l; },
get height() { return this._b - this._t; },
get x() { return this.left; },
get y() { return this.top; },
get width() { return this.right - this.left; },
get height() { return this.bottom - this.top; },
set x(v) {
let diff = this._l - v;
this._l = v;
this._r -= diff;
let diff = this.left - v;
this.left = v;
this.right -= diff;
},
set y(v) {
let diff = this._t - v;
this._t = v;
this._b -= diff;
let diff = this.top - v;
this.top = v;
this.bottom -= diff;
},
set width(v) { this._r = this._l + v; },
set height(v) { this._b = this._t + v; },
get left() { return this._l; },
get right() { return this._r; },
get top() { return this._t; },
get bottom() { return this._b; },
set left(v) { this._l = v; },
set right(v) { this._r = v; },
set top(v) { this._t = v; },
set bottom(v) { this._b = v; },
set width(v) { this.right = this.left + v; },
set height(v) { this.bottom = this.top + v; },
setRect: function(x, y, w, h) {
this._l = x;
this._t = y;
this._r = x+w;
this._b = y+h;
this.left = x;
this.top = y;
this.right = x+w;
this.bottom = y+h;
return this;
},
setBounds: function(t, l, b, r) {
this._t = t;
this._l = l;
this._b = b;
this._r = r;
this.top = t;
this.left = l;
this.bottom = b;
this.right = r;
return this;
},
clone: function() {
return new wsRect(this._l, this._t, this.width, this.height);
equals: function equals(r) {
return (r != null &&
this.top == r.top &&
this.left == r.left &&
this.bottom == r.bottom &&
this.right == r.right);
},
clone: function clone() {
return new wsRect(this.left, this.top, this.right - this.left, this.bottom - this.top);
},
center: function center() {
return [this.left + (this.right - this.left) / 2,
this.top + (this.bottom - this.top) / 2];
},
centerRounded: function centerRounded() {
return this.center().map(Math.round);
},
copyFrom: function(r) {
this._t = r._t;
this._l = r._l;
this._b = r._b;
this._r = r._r;
this.top = r.top;
this.left = r.left;
this.bottom = r.bottom;
this.right = r.right;
return this;
},
copyFromTLBR: function(r) {
this._l = r.left;
this._t = r.top;
this._r = r.right;
this._b = r.bottom;
this.left = r.left;
this.top = r.top;
this.right = r.right;
this.bottom = r.bottom;
return this;
},
translate: function(x, y) {
this._l += x;
this._r += x;
this._t += y;
this._b += y;
this.left += x;
this.right += x;
this.top += y;
this.bottom += y;
return this;
},
// return a new wsRect that is the union of that one and this one
union: function(rect) {
let l = Math.min(this._l, rect._l);
let r = Math.max(this._r, rect._r);
let t = Math.min(this._t, rect._t);
let b = Math.max(this._b, rect._b);
let l = Math.min(this.left, rect.left);
let r = Math.max(this.right, rect.right);
let t = Math.min(this.top, rect.top);
let b = Math.max(this.bottom, rect.bottom);
return new wsRect(l, t, r-l, b-t);
},
@ -219,25 +219,25 @@ wsRect.prototype = {
},
expandBy: function(b) {
this._l += b.left;
this._r += b.right;
this._t += b.top;
this._b += b.bottom;
this.left += b.left;
this.right += b.right;
this.top += b.top;
this.bottom += b.bottom;
return this;
},
contains: function(other) {
return !!(other._l >= this._l &&
other._r <= this._r &&
other._t >= this._t &&
other._b <= this._b);
return !!(other.left >= this.left &&
other.right <= this.right &&
other.top >= this.top &&
other.bottom <= this.bottom);
},
intersect: function(r2) {
let xmost1 = this._r;
let xmost2 = r2._r;
let xmost1 = this.right;
let xmost2 = r2.right;
let x = Math.max(this._l, r2._l);
let x = Math.max(this.left, r2.left);
let temp = Math.min(xmost1, xmost2);
if (temp <= x)
@ -245,9 +245,9 @@ wsRect.prototype = {
let width = temp - x;
let ymost1 = this._b;
let ymost2 = r2._b;
let y = Math.max(this._t, r2._t);
let ymost1 = this.bottom;
let ymost2 = r2.bottom;
let y = Math.max(this.top, r2.top);
temp = Math.min(ymost1, ymost2);
if (temp <= y)
@ -259,12 +259,12 @@ wsRect.prototype = {
},
intersects: function(other) {
let xok = (other._l > this._l && other._l < this._r) ||
(other._r > this._l && other._r < this._r) ||
(other._l <= this._l && other._r >= this._r);
let yok = (other._t > this._t && other._t < this._b) ||
(other._b > this._t && other._b < this._b) ||
(other._t <= this._t && other._b >= this._b);
let xok = (other.left > this.left && other.left < this.right) ||
(other.right > this.left && other.right < this.right) ||
(other.left <= this.left && other.right >= this.right);
let yok = (other.top > this.top && other.top < this.bottom) ||
(other.bottom > this.top && other.bottom < this.bottom) ||
(other.top <= this.top && other.bottom >= this.bottom);
return xok && yok;
},
@ -274,10 +274,10 @@ wsRect.prototype = {
* of returning a new rect.
*/
restrictTo: function restrictTo(r2) {
let xmost1 = this._r;
let xmost2 = r2._r;
let xmost1 = this.right;
let xmost2 = r2.right;
let x = Math.max(this._l, r2._l);
let x = Math.max(this.left, r2.left);
let temp = Math.min(xmost1, xmost2);
if (temp <= x)
@ -285,9 +285,9 @@ wsRect.prototype = {
let width = temp - x;
let ymost1 = this._b;
let ymost2 = r2._b;
let y = Math.max(this._t, r2._t);
let ymost1 = this.bottom;
let ymost2 = r2.bottom;
let y = Math.max(this.top, r2.top);
temp = Math.min(ymost1, ymost2);
if (temp <= y)
@ -307,10 +307,10 @@ wsRect.prototype = {
* given a strict subset rect as the argument.
*/
expandToContain: function extendTo(rect) {
let l = Math.min(this._l, rect._l);
let r = Math.max(this._r, rect._r);
let t = Math.min(this._t, rect._t);
let b = Math.max(this._b, rect._b);
let l = Math.min(this.left, rect.left);
let r = Math.max(this.right, rect.right);
let t = Math.min(this.top, rect.top);
let b = Math.max(this.bottom, rect.bottom);
return this.setRect(l, t, r-l, b-t);
},
@ -318,17 +318,19 @@ wsRect.prototype = {
round: function round(scale) {
if (!scale) scale = 1;
this._l = Math.floor(this._l * scale) / scale;
this._t = Math.floor(this._t * scale) / scale;
this._r = Math.ceil(this._r * scale) / scale;
this._b = Math.ceil(this._b * scale) / scale;
this.left = Math.floor(this.left * scale) / scale;
this.top = Math.floor(this.top * scale) / scale;
this.right = Math.ceil(this.right * scale) / scale;
this.bottom = Math.ceil(this.bottom * scale) / scale;
return this;
},
scale: function scale(xscl, yscl) {
this._l *= xscl;
this._r *= xscl;
this._t *= yscl;
this._b *= yscl;
this.left *= xscl;
this.right *= xscl;
this.top *= yscl;
this.bottom *= yscl;
return this;
}

View File

@ -0,0 +1,469 @@
<window
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:html="http://www.w3.org/1999/xhtml"
onload="onAlmostLoad();"
style="background-color:white;"
width="800"
height="480"
onresize="onResize();"
onkeypress="onKeyPress(event);">
<script type="application/x-javascript" src="WidgetStack.js"/>
<script type="application/x-javascript" src="TileManager.js"/>
<script type="application/x-javascript;version=1.8" src="BrowserView.js"/>
<script type="application/x-javascript;version=1.8">
<![CDATA[
// We do not endorse the use of globals, but this is just a closed lab
// environment. What could possibly go wrong? ...
let bv = null;
let scrollbox = null;
let leftbar = null;
let rightbar = null;
let topbar = null;
function debug() {
let w = {};
let h = {};
scrollbox.getScrolledSize(w, h);
let container = document.getElementById("tile_container");
let [x, y] = getScrollboxPosition();
let [w, h] = [w.value, h.value];
if (bv) {
dump('----------------------DEBUG!-------------------------\n');
dump(bv._browserViewportState.toString() + endl);
dump(endl);
let cr = bv._tileManager._criticalRect;
dump('criticalRect from BV: ' + (cr ? cr.toString() : null) + endl);
dump('visibleRect from BV : ' + bv._visibleRect.toString() + endl);
dump('visibleRect from foo: ' + scrollboxToViewportRect(getVisibleRect()) + endl);
dump(endl);
dump('container width,height from BV: ' + bv._container.style.width + ', '
+ bv._container.style.height + endl);
dump('container width,height via DOM: ' + container.style.width + ', '
+ container.style.height + endl);
dump(endl);
dump('scrollbox position : ' + x + ', ' + y + endl);
dump('scrollbox scrolledsize: ' + w + ', ' + h + endl);
dump(endl);
dump('tilecache capacity: ' + bv._tileManager._tileCache.getCapacity() + endl);
dump('tilecache size : ' + bv._tileManager._tileCache.size + endl);
dump('tilecache numFree : ' + bv._tileManager._tileCache.numFree + endl);
dump('tilecache iBound : ' + bv._tileManager._tileCache.iBound + endl);
dump('tilecache jBound : ' + bv._tileManager._tileCache.jBound + endl);
dump('tilecache _lru : ' + bv._tileManager._tileCache._lru + endl);
dump('-----------------------------------------------------\n');
}
}
function debugTile(i, j) {
let tc = bv._tileManager._tileCache;
let t = tc.getTile(i, j);
dump('------ DEBUGGING TILE (' + i + ',' + j + ') --------\n');
dump('in bounds: ' + tc.inBounds(i, j) + endl);
dump('occupied : ' + tc._isOccupied(i, j) + endl);
if (t)
{
dump('toString : ' + t.toString(true) + endl);
dump('free : ' + t.free + endl);
dump('dirtyRect: ' + t._dirtyTileCanvasRect + endl);
let len = tc._tilePool.length;
for (let k = 0; k < len; ++k)
if (tc._tilePool[k] === t)
dump('found in tilePool at index ' + k + endl);
}
dump('------------------------------------\n');
}
function onKeyPress(e) {
const a = 97; // debug all critical tiles
const c = 99; // set tilecache capacity
const d = 100; // debug dump
const f = 102; // run noop() through forEachIntersectingRect (for timing)
const i = 105; // toggle info click mode
const l = 108; // restart lazy crawl
const m = 109; // fix mouseout
const t = 116; // debug given list of tiles separated by space
switch (e.charCode) {
case d:
debug();
break;
case l:
bv._tileManager.restartLazyCrawl(bv._tileManager._criticalRect);
break;
case c:
let cap = parseInt(window.prompt('new capacity'));
bv._tileManager._tileCache.setCapacity(cap);
break;
case f:
let noop = function noop() { for (let i = 0; i < 10; ++i); };
bv._tileManager._tileCache.forEachIntersectingRect(bv._tileManager._criticalRect,
false, noop, window);
break;
case t:
let ijstrs = window.prompt('row,col plz').split(' ');
for each (let ijstr in ijstrs) {
let [i, j] = ijstr.split(',').map(function (x) parseInt(x));
debugTile(i, j);
}
break;
case a:
let cr = bv._tileManager._criticalRect;
dump('>>>>>> critical rect is ' + (cr ? cr.toString() : cr) + endl);
if (cr) {
let starti = cr.left >> kTileExponentWidth;
let endi = cr.right >> kTileExponentWidth;
let startj = cr.top >> kTileExponentHeight;
let endj = cr.bottom >> kTileExponentHeight;
for (var jj = startj; jj <= endj; ++jj)
for (var ii = starti; ii <= endi; ++ii)
debugTile(ii, jj);
}
break;
case i:
window.infoMode = !window.infoMode;
break;
case m:
onMouseUp();
break;
default:
break;
}
}
function onResize(e) {
if (bv) {
bv.beginBatchOperation();
bv.setVisibleRect(scrollboxToViewportRect(getVisibleRect()));
bv.zoomToPage();
bv.commitBatchOperation();
}
}
function onMouseDown(e) {
if (window.infoMode) {
let [basex, basey] = getScrollboxPosition();
let [x, y] = scrollboxToViewportXY(basex + e.clientX, basey + e.clientY);
let i = x >> kTileExponentWidth;
let j = y >> kTileExponentHeight;
debugTile(i, j);
}
window._isDragging = true;
window._dragStart = {x: e.clientX, y: e.clientY};
bv.pauseRendering();
}
function onMouseUp() {
window._isDragging = false;
bv.resumeRendering();
}
function onMouseMove(e) {
if (window._isDragging) {
let x = {};
let y = {};
let w = {};
let h = {};
scrollbox.getPosition(x, y);
scrollbox.getScrolledSize(w, h);
let dx = window._dragStart.x - e.clientX;
let dy = window._dragStart.y - e.clientY;
// XXX if max(x, 0) > scrollwidth we shouldn't do anything (same for y/height)
let newX = Math.max(x.value + dx, 0);
let newY = Math.max(y.value + dy, 0);
if (newX < w.value || newY < h.value) {
// clip dx and dy to prevent us from going below 0
dx = Math.max(dx, -x.value);
dy = Math.max(dy, -y.value);
let oldx = x.value;
let oldy = y.value;
bv.onBeforeVisibleMove(dx, dy);
updateBars(oldx, oldy, dx, dy);
scrollbox.scrollBy(dx, dy);
let [newx, newy] = getScrollboxPosition();
let realdx = newx - oldx;
let realdy = newy - oldy;
updateBars(oldx, oldy, realdx, realdy);
bv.onAfterVisibleMove(realdx, realdy);
}
window._dragStart = {x: e.clientX, y: e.clientY};
}
}
function onAlmostLoad() {
window._isDragging = false;
window.infoMode = false;
window.setTimeout(onLoad, 1500);
}
function onLoad() {
// ----------------------------------------------------
scrollbox = document.getElementById("scrollbox")
.boxObject
.QueryInterface(Components.interfaces.nsIScrollBoxObject);
leftbar = document.getElementById("left_sidebar");
rightbar = document.getElementById("right_sidebar");
topbar = document.getElementById("top_urlbar");
// ----------------------------------------------------
let initX = Math.round(leftbar.getBoundingClientRect().right);
dump('scrolling to ' + initX + endl);
scrollbox.scrollTo(initX, 0);
let [x, y] = getScrollboxPosition();
dump(' scrolled to ' + x + ',' + y + endl);
let container = document.getElementById("tile_container");
container.addEventListener("mousedown", onMouseDown, true);
container.addEventListener("mouseup", onMouseUp, true);
container.addEventListener("mousemove", onMouseMove, true);
bv = new BrowserView(container, scrollboxToViewportRect(getVisibleRect()));
let browser = document.getElementById("googlenews");
bv.setBrowser(browser, false);
}
function updateBars(x, y, dx, dy) {
return;
// shouldn't update margin if it doesn't need to be changed
let sidebars = document.getElementsByClassName("sidebar");
for (let i = 0; i < sidebars.length; i++) {
let sidebar = sidebars[i];
sidebar.style.margin = (y + dy) + "px 0px 0px 0px";
}
let urlbar = document.getElementById("top_urlbar");
urlbar.style.margin = "0px 0px 0px " + (x + dx) + "px";
}
function viewportToScrollboxXY(x, y) {
return scrollboxToViewportXY(x, y, -1);
}
function scrollboxToViewportXY(x, y) {
if (!x) x = 0;
if (!y) y = 0;
// shield your eyes!
let direction = (arguments.length >= 3) ? arguments[2] : 1;
let leftbarcr = leftbar.getBoundingClientRect();
let rightbarcr = rightbar.getBoundingClientRect();
let topbarcr = topbar.getBoundingClientRect();
let xtrans = direction * (-leftbarcr.width);
let ytrans = direction * (-topbarcr.height);
x += xtrans;
y += ytrans;
return [x, y];
}
function scrollboxToBrowserXY(browserView, x, y) {
[x, y] = scrollboxToViewportXY(x, y);
return [browserView.viewportToBrowser(x),
browserView.viewportToBrowser(y)];
}
function scrollboxToViewportRect(rect) {
let leftbarcr = leftbar.getBoundingClientRect();
let topbarcr = topbar.getBoundingClientRect();
let xtrans = -leftbarcr.width;
let ytrans = -topbarcr.height;
rect.translate(xtrans, ytrans);
return rect;
}
function getScrollboxPosition() {
let x = {};
let y = {};
scrollbox.getPosition(x, y);
return [x.value, y.value];
}
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 getBrowserClientRect(browser, el) {
let [scrollX, scrollY] = getContentScrollValues(browser);
let r = el.getBoundingClientRect();
return new wsRect(r.left + scrollX,
r.top + scrollY,
r.width, r.height);
}
function scrollToElement(browser, el) {
var elRect = getPagePosition(browser, el);
bv.browserToViewportRect(elRect);
elRect.round();
this.scrollTo(elRect.x, elRect.y);
}
function zoomToElement(aElement) {
const margin = 15;
let elRect = getBrowserClientRect(browser, aElement);
let elWidth = elRect.width;
let vrWidth = bv.visibleRect.width;
/* Try to set zoom-level such that once zoomed element is as wide
* as the visible rect */
let zoomLevel = vrtWidth / (elWidth + (2 * margin));
bv.beginBatchOperation();
bv.setZoomLevel(zoomLevel);
/* If zoomLevel ends up clamped to less than asked for, calculate
* how many more screen pixels will fit horizontally in addition to
* element's width. This ensures that more of the webpage is
* showing instead of the navbar. Bug 480595. */
let xpadding = Math.max(margin, vrWidth - bv.browserToViewport(elWidth));
// XXX TODO these arguments are wrong, we still have to transform the coordinates
// from viewport to scrollbox before sending them to scrollTo
this.scrollTo(Math.floor(Math.max(bv.browserToViewport(elRect.x) - xpadding, 0)),
Math.floor(Math.max(bv.browserToViewport(elRect.y) - margin, 0)));
bv.commitBatchOperation();
}
function zoomFromElement(browser, aElement) {
let elRect = getBrowserClientRect(browser, aElement);
bv.beginBatchOperation();
// pan to the element
// don't bother with x since we're zooming all the way out
bv.zoomToPage();
// XXX have this center the element on the page
// XXX TODO these arguments are wrong, we still have to transform the coordinates
// from viewport to scrollbox before sending them to scrollTo
this.scrollTo(0, Math.floor(Math.max(0, bv.browserToViewport(elRect.y))));
bv.commitBatchOperation();
}
/**
* Retrieve the content element for a given point in client coordinates
* (relative to the top left corner of the chrome window).
*/
function elementFromPoint(browser, browserView, x, y) {
[x, y] = scrollboxToBrowserXY(browserView, x, y);
let cwu = getBrowserDOMWindowUtils(browser);
return cwu.elementFromPoint(x, y,
true, /* ignore root scroll frame*/
false); /* don't flush layout */
}
/* ensures that a given content element is visible */
function ensureElementIsVisible(browser, aElement) {
let elRect = getBrowserClientRect(browser, aElement);
bv.browserToViewportRect(elRect);
let curRect = bv.visibleRect;
let newx = curRect.x;
let newy = curRect.y;
if (elRect.x < curRect.x || elRect.width > curRect.width) {
newx = elRect.x;
} else if (elRect.x + elRect.width > curRect.x + curRect.width) {
newx = elRect.x - curRect.width + elRect.width;
}
if (elRect.y < curRect.y || elRect.height > curRect.height) {
newy = elRect.y;
} else if (elRect.y + elRect.height > curRect.y + curRect.height) {
newy = elRect.y - curRect.height + elRect.height;
}
// XXX TODO these arguments are wrong, we still have to transform the coordinates
// from viewport to scrollbox before sending them to scrollTo
this.scrollTo(newx, newy);
}
// this is a mehful way of getting the visible rect in scrollbox coordinates
// that we use in this here lab environment and hopefully nowhere in real fennec
function getVisibleRect() {
let w = window.innerWidth;
let h = window.innerHeight;
let [x, y] = getScrollboxPosition();
return new wsRect(x, y, w, h);
}
]]>
</script>
<scrollbox id="scrollbox" style="-moz-box-orient: vertical; overflow: scroll;" flex="1">
<hbox id="top_urlbar" style="background-color: pink"><textbox flex="1"/></hbox>
<hbox style="position: relative">
<vbox id="left_sidebar" class="sidebar" style="background-color: red"><button label="left sidebar"/></vbox>
<box>
<html:div id="tile_container" style="position: relative; width: 800px; height: 480px; overflow: -moz-hidden-unscrollable;"/>
</box>
<vbox id="right_sidebar" class="sidebar" style="background-color: blue"><button label="right sidebar"/></vbox>
</hbox>
</scrollbox>
<box>
<html:div style="position: relative; overflow: hidden; max-width: 0px; max-height: 0px; visibility: hidden;">
<html:div id="browsers" style="position: absolute;">
<!-- <browser id="googlenews" src="http://www.webhamster.com/" type="content" style="width: 1024px; height: 614px"/> -->
<browser id="googlenews" src="http://slashdot.org/" type="content" style="width: 1024px; height: 614px"/>
</html:div>
</html:div>
</box>
</window>