summaryrefslogtreecommitdiffstats
path: root/toolkit/content/tests/fennec-tile-testapp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/content/tests/fennec-tile-testapp')
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/application.ini11
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/chrome.manifest1
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js694
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/content/FooScript.js352
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/content/TileManager.js1018
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js1438
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/content/firefoxOverlay.xul15
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/content/foo.xul460
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/content/main.xul7
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/content/overlay.js15
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.dtd1
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.properties3
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/chrome/skin/overlay.css5
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/defaults/preferences/prefs.js2
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/install.rdf17
-rw-r--r--toolkit/content/tests/fennec-tile-testapp/logread.py104
16 files changed, 4143 insertions, 0 deletions
diff --git a/toolkit/content/tests/fennec-tile-testapp/application.ini b/toolkit/content/tests/fennec-tile-testapp/application.ini
new file mode 100644
index 000000000..510d48180
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/application.ini
@@ -0,0 +1,11 @@
+[App]
+Vendor=venderr
+Name=tile
+Version=1.0
+BuildID=20060101
+Copyright=Copyright (c) 2006 Mark Finkle
+ID=xulapp@starkravingfinkle.org
+
+[Gecko]
+MinVersion=1.8
+MaxVersion=2.0
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/chrome.manifest b/toolkit/content/tests/fennec-tile-testapp/chrome/chrome.manifest
new file mode 100644
index 000000000..118354c81
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/chrome.manifest
@@ -0,0 +1 @@
+content tile file:content/
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js
new file mode 100644
index 000000000..c498810df
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js
@@ -0,0 +1,694 @@
+// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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/. */
+
+var Ci = Components.interfaces;
+
+// --- REMOVE ---
+var noop = function() {};
+var 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) {
+ return [browser.scrollWidth, browser.scrollHeight];
+ }
+
+ 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';
+ }
+
+ // !!! --- RESIZE HACK BEGIN -----
+ function simulateMozAfterSizeChange(browser, width, height) {
+ let ev = document.createElement("MouseEvents");
+ ev.initEvent("FakeMozAfterSizeChange", false, false, window, 0, width, height);
+ browser.dispatchEvent(ev);
+ }
+ // !!! --- RESIZE HACK END -------
+
+ // --- 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, skipZoom) {
+ 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 (!skipZoom) {
+ 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.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)
+ simulateMozAfterSizeChange(browser, hack.maxW, hack.maxH);
+ // !!! --- RESIZE HACK END -------
+
+ tm.dirtyRects(rects, this.isRendering());
+ },
+
+ 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] = 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";
+ */
+
+ 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}';
+ }
+
+};
+
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/FooScript.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/FooScript.js
new file mode 100644
index 000000000..49cbbed66
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/FooScript.js
@@ -0,0 +1,352 @@
+var noop = function() {};
+Browser = {
+ updateViewportSize: noop
+ /** ***********************************************************
+ function
+ let browser = document.getElementById("googlenews");
+ let cdoc = browser.contentDocument;
+
+ // These might not exist yet depending on page load state
+ var body = cdoc.body || {};
+ var html = cdoc.documentElement || {};
+
+ var w = Math.max(body.scrollWidth || 0, html.scrollWidth);
+ var h = Math.max(body.scrollHeight || 0, html.scrollHeight);
+
+ window.tileManager.viewportHandler(new wsRect(0, 0, w, h),
+ window.innerWidth,
+ new wsRect(0, 0, window.innerWidth, window.innerHeight),
+ false);
+ *************************************************************/
+};
+var ws = {
+ beginUpdateBatch: noop,
+ panTo: noop,
+ endUpdateBatch: noop
+};
+var Ci = Components.interfaces;
+var bv = null;
+var endl = "\n";
+
+
+function BrowserView() {
+ this.init();
+ bindAll(this);
+}
+
+BrowserView.prototype = {
+
+ // --- PROPERTIES ---
+ // public:
+ // init()
+ // getViewportInnerBoundsRect(dx, dy)
+ // tileManager
+ // scrollbox
+ //
+ // private:
+ // _scrollbox
+ // _leftbar
+ // _rightbar
+ // _topbar
+ // _browser
+ // _tileManager
+ // _viewportRect
+ // _viewportInnerBoundsRect
+ //
+
+ get tileManager() { return this._tileManager; },
+ get scrollbox() { return this._scrollbox; },
+
+ init: function init() {
+ let scrollbox = document.getElementById("scrollbox").boxObject;
+ this._scrollbox = scrollbox;
+
+ let leftbar = document.getElementById("left_sidebar");
+ let rightbar = document.getElementById("right_sidebar");
+ let topbar = document.getElementById("top_urlbar");
+ this._leftbar = leftbar;
+ this._rightbar = rightbar;
+ this._topbar = topbar;
+
+ scrollbox.scrollTo(Math.round(leftbar.getBoundingClientRect().right), 0);
+
+ let tileContainer = document.getElementById("tile_container");
+ tileContainer.addEventListener("mousedown", onMouseDown, true);
+ tileContainer.addEventListener("mouseup", onMouseUp, true);
+ tileContainer.addEventListener("mousemove", onMouseMove, true);
+ this._tileContainer = tileContainer;
+
+ let tileManager = new TileManager(this.appendTile, this.removeTile, window.innerWidth);
+ this._tileManager = tileManager;
+
+ let browser = document.getElementById("googlenews");
+ this.setCurrentBrowser(browser, false); // sets this._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);
+
+ let viewportRect = new wsRect(0, 0, w, h);
+ this._viewportRect = viewportRect;
+
+ let viewportInnerBoundsRect = this.getViewportInnerBoundsRect();
+ this._viewportInnerBoundsRect = viewportInnerBoundsRect;
+
+ tileManager.viewportHandler(viewportRect,
+ window.innerWidth,
+ viewportInnerBoundsRect,
+ true);
+ },
+
+ resizeTileContainer: function resizeTileContainer() {
+
+ },
+
+ scrollboxToViewportRect: function scrollboxToViewportRect(rect, clip) {
+ let leftbar = this._leftbar.getBoundingClientRect();
+ let rightbar = this._rightbar.getBoundingClientRect();
+ let topbar = this._topbar.getBoundingClientRect();
+
+ let xtrans = -leftbar.width;
+ let ytrans = -topbar.height;
+ let x = rect.x + xtrans;
+ let y = rect.y + ytrans;
+
+ // XXX we're cheating --- this is not really a clip, but its the only
+ // way this function is used
+ rect.x = (clip) ? Math.max(x, 0) : x;
+ rect.y = (clip) ? Math.max(y, 0) : y;
+
+ return rect;
+ },
+
+ getScrollboxPosition: function getScrollboxPosition() {
+ return [this._scrollbox.positionX, this._scrollbox.positionY];
+ },
+
+ getViewportInnerBoundsRect: function getViewportInnerBoundsRect(dx, dy) {
+ if (!dx) dx = 0;
+ if (!dy) dy = 0;
+
+ let w = window.innerWidth;
+ let h = window.innerHeight;
+
+ let leftbar = this._leftbar.getBoundingClientRect();
+ let rightbar = this._rightbar.getBoundingClientRect();
+ let topbar = this._topbar.getBoundingClientRect();
+
+ let leftinner = Math.max(leftbar.right - dx, 0);
+ let rightinner = Math.min(rightbar.left - dx, w);
+ let topinner = Math.max(topbar.bottom - dy, 0);
+
+ let [x, y] = this.getScrollboxPosition();
+
+ return this.scrollboxToViewportRect(new wsRect(x + dx, y + dy, rightinner - leftinner, h - topinner),
+ true);
+ },
+
+ appendTile: function appendTile(tile) {
+ let canvas = tile.contentImage;
+
+ canvas.style.position = "absolute";
+ canvas.style.left = tile.x + "px";
+ canvas.style.top = tile.y + "px";
+
+ let tileContainer = document.getElementById("tile_container");
+ tileContainer.appendChild(canvas);
+
+ dump('++ ' + tile.toString() + endl);
+ },
+
+ removeTile: function removeTile(tile) {
+ let canvas = tile.contentImage;
+
+ let tileContainer = document.getElementById("tile_container");
+ tileContainer.removeChild(canvas);
+
+ dump('-- ' + tile.toString() + endl);
+ },
+
+ scrollBy: function scrollBy(dx, dy) {
+ // TODO
+ this.onBeforeScroll();
+ this.onAfterScroll();
+ },
+
+ // x: current x
+ // y: current y
+ // dx: delta to get to x from current x
+ // dy: delta to get to y from current y
+ onBeforeScroll: function onBeforeScroll(x, y, dx, dy) {
+ this.tileManager.onBeforeScroll(this.getViewportInnerBoundsRect(dx, dy));
+
+ // 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";
+ },
+
+ onAfterScroll: function onAfterScroll(x, y, dx, dy) {
+ this.tileManager.onAfterScroll(this.getViewportInnerBoundsRect());
+ },
+
+ setCurrentBrowser: function setCurrentBrowser(browser, skipZoom) {
+ let currentBrowser = this._browser;
+ if (currentBrowser) {
+ // backup state
+ currentBrowser.mZoomLevel = this.zoomLevel;
+ currentBrowser.mPanX = ws._viewingRect.x;
+ currentBrowser.mPanY = ws._viewingRect.y;
+
+ // stop monitor paint events for this browser
+ currentBrowser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
+ currentBrowser.setAttribute("type", "content");
+ currentBrowser.docShell.isOffScreenBrowser = false;
+ }
+
+ browser.setAttribute("type", "content-primary");
+ if (!skipZoom)
+ browser.docShell.isOffScreenBrowser = true;
+
+ // start monitoring paint events for this browser
+ browser.addEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
+
+ this._browser = browser;
+
+ // endLoading(and startLoading in most cases) calls zoom anyway
+ if (!skipZoom) {
+ this.zoomToPage();
+ }
+
+ if ("mZoomLevel" in browser) {
+ // restore last state
+ ws.beginUpdateBatch();
+ ws.panTo(browser.mPanX, browser.mPanY);
+ this.zoomLevel = browser.mZoomLevel;
+ ws.endUpdateBatch(true);
+
+ // drop the cache
+ delete browser.mZoomLevel;
+ delete browser.mPanX;
+ delete browser.mPanY;
+ }
+
+ this.tileManager.browser = browser;
+ },
+
+ handleMozAfterPaint: function handleMozAfterPaint(ev) {
+ this.tileManager.handleMozAfterPaint(ev);
+ },
+
+ zoomToPage: function zoomToPage() {
+ /** ******************************************************
+ let needToPanToTop = this._needToPanToTop;
+ // Ensure pages are panned at the top before zooming/painting
+ // combine the initial pan + zoom into a transaction
+ if (needToPanToTop) {
+ ws.beginUpdateBatch();
+ this._needToPanToTop = false;
+ ws.panTo(0, -BrowserUI.toolbarH);
+ }
+ // Adjust the zoomLevel to fit the page contents in our window width
+ let [contentW, ] = this._contentAreaDimensions;
+ let fakeW = this._fakeWidth;
+
+ if (contentW > fakeW)
+ this.zoomLevel = fakeW / contentW;
+
+ if (needToPanToTop)
+ ws.endUpdateBatch();
+ ********************************************************/
+ }
+
+};
+
+
+function onResize(e) {
+ let browser = document.getElementById("googlenews");
+ let cdoc = browser.contentDocument;
+
+ // These might not exist yet depending on page load state
+ var body = cdoc.body || {};
+ var html = cdoc.documentElement || {};
+
+ var w = Math.max(body.scrollWidth || 0, html.scrollWidth);
+ var h = Math.max(body.scrollHeight || 0, html.scrollHeight);
+
+ if (bv)
+ bv.tileManager.viewportHandler(new wsRect(0, 0, w, h),
+ window.innerWidth,
+ bv.getViewportInnerBoundsRect(),
+ true);
+}
+
+function onMouseDown(e) {
+ window._isDragging = true;
+ window._dragStart = {x: e.clientX, y: e.clientY};
+
+ bv.tileManager.startPanning();
+}
+
+function onMouseUp() {
+ window._isDragging = false;
+
+ bv.tileManager.endPanning();
+}
+
+function onMouseMove(e) {
+ if (window._isDragging) {
+ let scrollbox = bv.scrollbox;
+
+ let x = scrollbox.positionX;
+ let y = scrollbox.positionY;
+ let w = scrollbox.scrolledWidth;
+ let h = scrollbox.scrolledHeight;
+
+ 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 + dx, 0);
+ let newY = Math.max(y + dy, 0);
+
+ if (newX < w || newY < h) {
+ // clip dx and dy to prevent us from going below 0
+ dx = Math.max(dx, -x);
+ dy = Math.max(dy, -y);
+
+ bv.onBeforeScroll(x, y, dx, dy);
+
+ /* dump("==========scroll==========" + endl);
+ dump("delta: " + dx + "," + dy + endl);
+ let xx = {};
+ let yy = {};
+ scrollbox.getPosition(xx, yy);
+ dump(xx.value + "," + yy.value + endl);*/
+
+ scrollbox.scrollBy(dx, dy);
+
+ /* scrollbox.getPosition(xx, yy);
+ dump(xx.value + "," + yy.value + endl);
+ dump("==========================" + endl);*/
+
+ bv.onAfterScroll();
+ }
+ }
+
+ window._dragStart = {x: e.clientX, y: e.clientY};
+}
+
+function onLoad() {
+ bv = new BrowserView();
+}
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/TileManager.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/TileManager.js
new file mode 100644
index 000000000..52beb6e36
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/TileManager.js
@@ -0,0 +1,1018 @@
+// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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 kXHTMLNamespaceURI = "http://www.w3.org/1999/xhtml";
+
+// base-2 exponent for width, height of a single tile.
+const kTileExponentWidth = 7;
+const kTileExponentHeight = 7;
+const kTileWidth = Math.pow(2, kTileExponentWidth); // 2^7 = 128
+const kTileHeight = Math.pow(2, kTileExponentHeight); // 2^7 = 128
+const kLazyRoundTimeCap = 500; // millis
+
+
+function bind(f, thisObj) {
+ return function() {
+ return f.apply(thisObj, arguments);
+ };
+}
+
+function bindSome(instance, methodNames) {
+ for (let methodName of methodNames)
+ if (methodName in instance)
+ instance[methodName] = bind(instance[methodName], instance);
+}
+
+function bindAll(instance) {
+ for (let key in instance)
+ if (instance[key] instanceof Function)
+ instance[key] = bind(instance[key], instance);
+}
+
+
+/**
+ * The Tile Manager!
+ *
+ * @param appendTile The function the tile manager should call in order to
+ * "display" a tile (e.g. append it to the DOM). The argument to this
+ * function is a TileManager.Tile object.
+ * @param removeTile The function the tile manager should call in order to
+ * "undisplay" a tile (e.g. remove it from the DOM). The argument to this
+ * function is a TileManager.Tile object.
+ * @param fakeWidth The width of the widest possible visible rectangle, e.g.
+ * the width of the screen. This is used in setting the zoomLevel.
+ */
+function TileManager(appendTile, removeTile, browserView) {
+ // backref to the BrowserView object that owns us
+ this._browserView = browserView;
+
+ // callbacks to append / remove a tile to / from the parent
+ this._appendTile = appendTile;
+ this._removeTile = removeTile;
+
+ // tile cache holds tile objects and pools them under a given capacity
+ let self = this;
+ this._tileCache = new TileManager.TileCache(function(tile) { self._removeTileSafe(tile); },
+ -1, -1, 110);
+
+ // Rectangle within the viewport that is visible to the user. It is "critical"
+ // in the sense that it must be rendered as soon as it becomes dirty
+ this._criticalRect = null;
+
+ // Current <browser> DOM element, holding the content we wish to render.
+ // This is null when no browser is attached
+ this._browser = null;
+
+ // if we have an outstanding paint timeout, its value is stored here
+ // for cancelling when we end page loads
+ // this._drawTimeout = 0;
+ this._pageLoadResizerTimeout = 0;
+
+ // timeout of the non-visible-tiles-crawler to cache renders from the browser
+ this._idleTileCrawlerTimeout = 0;
+
+ // object that keeps state on our current lazyload crawl
+ this._crawler = null;
+
+ // the max right coordinate we've seen from paint events
+ // while we were loading a page. If we see something that's bigger than
+ // our width, we'll trigger a page zoom.
+ this._pageLoadMaxRight = 0;
+ this._pageLoadMaxBottom = 0;
+
+ // Tells us to pan to top before first draw
+ this._needToPanToTop = false;
+}
+
+TileManager.prototype = {
+
+ setBrowser: function setBrowser(b) { this._browser = b; },
+
+ // This is the callback fired by our client whenever the viewport
+ // changed somehow (or didn't change but someone asked it to update).
+ viewportChangeHandler: function viewportChangeHandler(viewportRect,
+ criticalRect,
+ boundsSizeChanged,
+ dirtyAll) {
+ // !!! --- DEBUG BEGIN -----
+ dump("***vphandler***\n");
+ dump(viewportRect.toString() + "\n");
+ dump(criticalRect.toString() + "\n");
+ dump(boundsSizeChanged + "\n");
+ dump(dirtyAll + "\n***************\n");
+ // !!! --- DEBUG END -------
+
+ let tc = this._tileCache;
+
+ tc.iBound = Math.ceil(viewportRect.right / kTileWidth);
+ tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight);
+
+ if (!criticalRect || !criticalRect.equals(this._criticalRect)) {
+ this.beginCriticalMove(criticalRect);
+ this.endCriticalMove(criticalRect, !boundsSizeChanged);
+ }
+
+ if (boundsSizeChanged) {
+ // TODO fastpath if !dirtyAll
+ this.dirtyRects([viewportRect.clone()], true);
+ }
+ },
+
+ dirtyRects: function dirtyRects(rects, doCriticalRender) {
+ let criticalIsDirty = false;
+ let criticalRect = this._criticalRect;
+
+ for (let rect of rects) {
+ this._tileCache.forEachIntersectingRect(rect, false, this._dirtyTile, this);
+
+ if (criticalRect && rect.intersects(criticalRect))
+ criticalIsDirty = true;
+ }
+
+ if (criticalIsDirty && doCriticalRender)
+ this.criticalRectPaint();
+ },
+
+ criticalRectPaint: function criticalRectPaint() {
+ let cr = this._criticalRect;
+
+ if (cr) {
+ let [ctrx, ctry] = cr.centerRounded();
+ this.recenterEvictionQueue(ctrx, ctry);
+ this._renderAppendHoldRect(cr);
+ }
+ },
+
+ beginCriticalMove2: function beginCriticalMove(destCriticalRect) {
+ let start = Date.now();
+ function appendNonDirtyTile(tile) {
+ if (!tile.isDirty())
+ this._appendTileSafe(tile);
+ }
+
+ if (destCriticalRect)
+ this._tileCache.forEachIntersectingRect(destCriticalRect, false, appendNonDirtyTile, this);
+ let end = Date.now();
+ dump("start: " + (end-start) + "\n")
+ },
+
+ beginCriticalMove: function beginCriticalMove(destCriticalRect) {
+ /*
+ function appendNonDirtyTile(tile) {
+ if (!tile.isDirty())
+ this._appendTileSafe(tile);
+ }
+ */
+
+ let start = Date.now();
+
+ if (destCriticalRect) {
+
+ let rect = destCriticalRect;
+
+ let create = false;
+
+ // this._tileCache.forEachIntersectingRect(destCriticalRect, false, appendNonDirtyTile, this);
+ let visited = {};
+ let evictGuard = null;
+ if (create) {
+ evictGuard = function evictGuard(tile) {
+ return !visited[tile.toString()];
+ };
+ }
+
+ let starti = rect.left >> kTileExponentWidth;
+ let endi = rect.right >> kTileExponentWidth;
+
+ let startj = rect.top >> kTileExponentHeight;
+ let endj = rect.bottom >> kTileExponentHeight;
+
+ let tile = null;
+ let tc = this._tileCache;
+
+ for (var j = startj; j <= endj; ++j) {
+ for (var i = starti; i <= endi; ++i) {
+
+ // 'this' for getTile needs to be tc
+
+ // tile = this.getTile(i, j, create, evictGuard);
+ // if (!tc.inBounds(i, j)) {
+ if (0 <= i && 0 <= j && i <= tc.iBound && j <= tc.jBound) {
+ // return null;
+ break;
+ }
+
+ tile = null;
+
+ // if (tc._isOccupied(i, j)) {
+ if (tc._tiles[i] && tc._tiles[i][j]) {
+ tile = tc._tiles[i][j];
+ } else if (create) {
+ // NOTE: create is false here
+ tile = tc._createTile(i, j, evictionGuard);
+ if (tile) tile.markDirty();
+ }
+
+ if (tile) {
+ visited[tile.toString()] = true;
+ // fn.call(thisObj, tile);
+ // function appendNonDirtyTile(tile) {
+ // if (!tile.isDirty())
+ if (!tile._dirtyTileCanvas) {
+ // this._appendTileSafe(tile);
+ if (!tile._appended) {
+ let astart = Date.now();
+ this._appendTile(tile);
+ tile._appended = true;
+ let aend = Date.now();
+ dump("append: " + (aend - astart) + "\n");
+ }
+ }
+ // }
+ }
+ }
+ }
+ }
+
+ let end = Date.now();
+ dump("start: " + (end-start) + "\n")
+ },
+
+ endCriticalMove: function endCriticalMove(destCriticalRect, doCriticalPaint) {
+ let start = Date.now();
+
+ let tc = this._tileCache;
+ let cr = this._criticalRect;
+
+ let dcr = destCriticalRect.clone();
+
+ let f = function releaseOldTile(tile) {
+ // release old tile
+ if (!tile.boundRect.intersects(dcr))
+ tc.releaseTile(tile);
+ }
+
+ if (cr)
+ tc.forEachIntersectingRect(cr, false, f, this);
+
+ this._holdRect(destCriticalRect);
+
+ if (cr)
+ cr.copyFrom(destCriticalRect);
+ else
+ this._criticalRect = cr = destCriticalRect;
+
+ let crpstart = Date.now();
+ if (doCriticalPaint)
+ this.criticalRectPaint();
+ dump(" crp: " + (Date.now() - crpstart) + "\n");
+
+ let end = Date.now();
+ dump("end: " + (end - start) + "\n");
+ },
+
+ restartLazyCrawl: function restartLazyCrawl(startRectOrQueue) {
+ if (!startRectOrQueue || startRectOrQueue instanceof Array) {
+ this._crawler = new TileManager.CrawlIterator(this._tileCache);
+
+ if (startRectOrQueue) {
+ let len = startRectOrQueue.length;
+ for (let k = 0; k < len; ++k)
+ this._crawler.enqueue(startRectOrQueue[k].i, startRectOrQueue[k].j);
+ }
+ } else {
+ this._crawler = new TileManager.CrawlIterator(this._tileCache, startRectOrQueue);
+ }
+
+ if (!this._idleTileCrawlerTimeout)
+ this._idleTileCrawlerTimeout = setTimeout(this._idleTileCrawler, 2000, this);
+ },
+
+ stopLazyCrawl: function stopLazyCrawl() {
+ this._idleTileCrawlerTimeout = 0;
+ this._crawler = null;
+
+ let cr = this._criticalRect;
+ if (cr) {
+ let [ctrx, ctry] = cr.centerRounded();
+ this.recenterEvictionQueue(ctrx, ctry);
+ }
+ },
+
+ recenterEvictionQueue: function recenterEvictionQueue(ctrx, ctry) {
+ let ctri = ctrx >> kTileExponentWidth;
+ let ctrj = ctry >> kTileExponentHeight;
+
+ function evictFarTiles(a, b) {
+ let dista = Math.max(Math.abs(a.i - ctri), Math.abs(a.j - ctrj));
+ let distb = Math.max(Math.abs(b.i - ctri), Math.abs(b.j - ctrj));
+ return dista - distb;
+ }
+
+ this._tileCache.sortEvictionQueue(evictFarTiles);
+ },
+
+ _renderTile: function _renderTile(tile) {
+ if (tile.isDirty())
+ tile.render(this._browser, this._browserView);
+ },
+
+ _appendTileSafe: function _appendTileSafe(tile) {
+ if (!tile._appended) {
+ this._appendTile(tile);
+ tile._appended = true;
+ }
+ },
+
+ _removeTileSafe: function _removeTileSafe(tile) {
+ if (tile._appended) {
+ this._removeTile(tile);
+ tile._appended = false;
+ }
+ },
+
+ _dirtyTile: function _dirtyTile(tile) {
+ if (!this._criticalRect || !tile.boundRect.intersects(this._criticalRect))
+ this._removeTileSafe(tile);
+
+ tile.markDirty();
+
+ if (this._crawler)
+ this._crawler.enqueue(tile.i, tile.j);
+ },
+
+ _holdRect: function _holdRect(rect) {
+ this._tileCache.holdTilesIntersectingRect(rect);
+ },
+
+ _releaseRect: function _releaseRect(rect) {
+ this._tileCache.releaseTilesIntersectingRect(rect);
+ },
+
+ _renderAppendHoldRect: function _renderAppendHoldRect(rect) {
+ function renderAppendHoldTile(tile) {
+ if (tile.isDirty())
+ this._renderTile(tile);
+
+ this._appendTileSafe(tile);
+ this._tileCache.holdTile(tile);
+ }
+
+ this._tileCache.forEachIntersectingRect(rect, true, renderAppendHoldTile, this);
+ },
+
+ _idleTileCrawler: function _idleTileCrawler(self) {
+ if (!self) self = this;
+ dump('crawl pass.\n');
+ let itered = 0, rendered = 0;
+
+ let start = Date.now();
+ let comeAgain = true;
+
+ while ((Date.now() - start) <= kLazyRoundTimeCap) {
+ let tile = self._crawler.next();
+
+ if (!tile) {
+ comeAgain = false;
+ break;
+ }
+
+ if (tile.isDirty()) {
+ self._renderTile(tile);
+ ++rendered;
+ }
+ ++itered;
+ }
+
+ dump('crawl itered:' + itered + ' rendered:' + rendered + '\n');
+
+ if (comeAgain) {
+ self._idleTileCrawlerTimeout = setTimeout(self._idleTileCrawler, 2000, self);
+ } else {
+ self.stopLazyCrawl();
+ dump('crawl end\n');
+ }
+ }
+
+};
+
+
+/**
+ * The tile cache used by the tile manager to hold and index all
+ * tiles. Also responsible for pooling tiles and maintaining the
+ * number of tiles under given capacity.
+ *
+ * @param onBeforeTileDetach callback set by the TileManager to call before
+ * we must "detach" a tile from a tileholder due to needing it elsewhere or
+ * having to discard it on capacity decrease
+ * @param capacity the initial capacity of the tile cache, i.e. the max number
+ * of tiles the cache can have allocated
+ */
+TileManager.TileCache = function TileCache(onBeforeTileDetach, iBound, jBound, capacity) {
+ if (arguments.length <= 3 || capacity < 0)
+ capacity = Infinity;
+
+ // We track all pooled tiles in a 2D array (row, column) ordered as
+ // they "appear on screen". The array is a grid that functions for
+ // storage of the tiles and as a lookup map. Each array entry is
+ // a reference to the tile occupying that space ("tileholder"). Entries
+ // are not unique, so a tile could be referenced by multiple array entries,
+ // i.e. a tile could "span" many tile placeholders (e.g. if we merge
+ // neighbouring tiles).
+ this._tiles = [];
+
+ // holds the same tiles that _tiles holds, but as contiguous array
+ // elements, for pooling tiles for reuse under finite capacity
+ this._tilePool = (capacity == Infinity) ? new Array() : new Array(capacity);
+
+ this._capacity = capacity;
+ this._nTiles = 0;
+ this._numFree = 0;
+
+ this._onBeforeTileDetach = onBeforeTileDetach;
+
+ this.iBound = iBound;
+ this.jBound = jBound;
+};
+
+TileManager.TileCache.prototype = {
+
+ get size() { return this._nTiles; },
+ get numFree() { return this._numFree; },
+
+ // A comparison function that will compare all free tiles as greater
+ // than all non-free tiles. Useful because, for instance, to shrink
+ // the tile pool when capacity is lowered, we want to remove all tiles
+ // at the new cap and beyond, favoring removal of free tiles first.
+ evictionCmp: function freeTilesLast(a, b) {
+ if (a.free == b.free) return (a.j == b.j) ? b.i - a.i : b.j - a.j;
+ return (a.free) ? 1 : -1;
+ },
+
+ getCapacity: function getCapacity() { return this._capacity; },
+
+ setCapacity: function setCapacity(newCap, skipEvictionQueueSort) {
+ if (newCap < 0)
+ throw "Cannot set a negative tile cache capacity";
+
+ if (newCap == Infinity) {
+ this._capacity = Infinity;
+ return;
+ } else if (this._capacity == Infinity) {
+ // pretend we had a finite capacity all along and proceed normally
+ this._capacity = this._tilePool.length;
+ }
+
+ let rem = null;
+
+ if (newCap < this._capacity) {
+ // This case is obnoxious. We're decreasing our capacity which means
+ // we may have to get rid of tiles. Depending on our eviction comparator,
+ // we probably try to get rid free tiles first, but we might have to throw
+ // out some nonfree ones too. Note that "throwing out" means the cache
+ // won't keep them, and they'll get GC'ed as soon as all other refholders
+ // let go of their refs to the tile.
+ if (!skipEvictionQueueSort)
+ this.sortEvictionQueue();
+
+ rem = this._tilePool.splice(newCap, this._tilePool.length);
+
+ } else {
+ // This case is win. Extend our tile pool array with new empty space.
+ this._tilePool.push.apply(this._tilePool, new Array(newCap - this._capacity));
+ }
+
+ // update state in the case that we threw things out.
+ let nTilesDeleted = this._nTiles - newCap;
+ if (nTilesDeleted > 0) {
+ let nFreeDeleted = 0;
+ for (let k = 0; k < nTilesDeleted; ++k) {
+ if (rem[k].free)
+ nFreeDeleted++;
+
+ this._detachTile(rem[k].i, rem[k].j);
+ }
+
+ this._nTiles -= nTilesDeleted;
+ this._numFree -= nFreeDeleted;
+ }
+
+ this._capacity = newCap;
+ },
+
+ _isOccupied: function _isOccupied(i, j) {
+ return !!(this._tiles[i] && this._tiles[i][j]);
+ },
+
+ _detachTile: function _detachTile(i, j) {
+ let tile = null;
+ if (this._isOccupied(i, j)) {
+ tile = this._tiles[i][j];
+
+ if (this._onBeforeTileDetach)
+ this._onBeforeTileDetach(tile);
+
+ this.releaseTile(tile);
+ delete this._tiles[i][j];
+ }
+ return tile;
+ },
+
+ _reassignTile: function _reassignTile(tile, i, j) {
+ this._detachTile(tile.i, tile.j); // detach
+ tile.init(i, j); // re-init
+ this._tiles[i][j] = tile; // attach
+ return tile;
+ },
+
+ _evictTile: function _evictTile(evictionGuard) {
+ let k = this._nTiles - 1;
+ let pool = this._tilePool;
+ let victim = null;
+
+ for (; k >= 0; --k) {
+ if (pool[k].free &&
+ (!evictionGuard || evictionGuard(pool[k])))
+ {
+ victim = pool[k];
+ break;
+ }
+ }
+
+ return victim;
+ },
+
+ _createTile: function _createTile(i, j, evictionGuard) {
+ if (!this._tiles[i])
+ this._tiles[i] = [];
+
+ let tile = null;
+
+ if (this._nTiles < this._capacity) {
+ // either capacity is infinite, or we still have room to allocate more
+ tile = new TileManager.Tile(i, j);
+ this._tiles[i][j] = tile;
+ this._tilePool[this._nTiles++] = tile;
+ this._numFree++;
+
+ } else {
+ // assert: nTiles == capacity
+ dump("\nevicting\n");
+ tile = this._evictTile(evictionGuard);
+ if (tile)
+ this._reassignTile(tile, i, j);
+ }
+
+ return tile;
+ },
+
+ inBounds: function inBounds(i, j) {
+ return 0 <= i && 0 <= j && i <= this.iBound && j <= this.jBound;
+ },
+
+ sortEvictionQueue: function sortEvictionQueue(cmp) {
+ if (!cmp) cmp = this.evictionCmp;
+ this._tilePool.sort(cmp);
+ },
+
+ /**
+ * Get a tile by its indices
+ *
+ * @param i Column
+ * @param j Row
+ * @param create Flag true if the tile should be created in case there is no
+ * tile at (i, j)
+ * @param reuseCondition Boolean-valued function to restrict conditions under
+ * which an old tile may be reused for creating this one. This can happen if
+ * the cache has reached its capacity and must reuse existing tiles in order to
+ * create this one. The function is given a Tile object as its argument and
+ * returns true if the tile is OK for reuse. This argument has no effect if the
+ * create argument is false.
+ */
+ getTile: function getTile(i, j, create, evictionGuard) {
+ if (!this.inBounds(i, j))
+ return null;
+
+ let tile = null;
+
+ if (this._isOccupied(i, j)) {
+ tile = this._tiles[i][j];
+ } else if (create) {
+ tile = this._createTile(i, j, evictionGuard);
+ if (tile) tile.markDirty();
+ }
+
+ return tile;
+ },
+
+ /**
+ * Look up (possibly creating) a tile from its viewport coordinates.
+ *
+ * @param x
+ * @param y
+ * @param create Flag true if the tile should be created in case it doesn't
+ * already exist at the tileholder corresponding to (x, y)
+ */
+ tileFromPoint: function tileFromPoint(x, y, create) {
+ let i = x >> kTileExponentWidth;
+ let j = y >> kTileExponentHeight;
+
+ return this.getTile(i, j, create);
+ },
+
+ /**
+ * Hold a tile (i.e. mark it non-free). Returns true if the operation
+ * actually did something, false elsewise.
+ */
+ holdTile: function holdTile(tile) {
+ if (tile && tile.free) {
+ tile._hold();
+ this._numFree--;
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Release a tile (i.e. mark it free). Returns true if the operation
+ * actually did something, false elsewise.
+ */
+ releaseTile: function releaseTile(tile) {
+ if (tile && !tile.free) {
+ tile._release();
+ this._numFree++;
+ return true;
+ }
+ return false;
+ },
+
+ // XXX the following two functions will iterate through duplicate tiles
+ // once we begin to merge tiles.
+ /**
+ * Fetch all tiles that share at least one point with this rect. If `create'
+ * is true then any tileless tileholders will have tiles created for them.
+ */
+ tilesIntersectingRect: function tilesIntersectingRect(rect, create) {
+ let dx = (rect.right % kTileWidth) - (rect.left % kTileWidth);
+ let dy = (rect.bottom % kTileHeight) - (rect.top % kTileHeight);
+ let tiles = [];
+
+ for (let y = rect.top; y <= rect.bottom - dy; y += kTileHeight) {
+ for (let x = rect.left; x <= rect.right - dx; x += kTileWidth) {
+ let tile = this.tileFromPoint(x, y, create);
+ if (tile)
+ tiles.push(tile);
+ }
+ }
+
+ return tiles;
+ },
+
+ forEachIntersectingRect: function forEachIntersectingRect(rect, create, fn, thisObj) {
+ let visited = {};
+ let evictGuard = null;
+ if (create) {
+ evictGuard = function evictGuard(tile) {
+ return !visited[tile.toString()];
+ };
+ }
+
+ let starti = rect.left >> kTileExponentWidth;
+ let endi = rect.right >> kTileExponentWidth;
+
+ let startj = rect.top >> kTileExponentHeight;
+ let endj = rect.bottom >> kTileExponentHeight;
+
+ let tile = null;
+ for (var j = startj; j <= endj; ++j) {
+ for (var i = starti; i <= endi; ++i) {
+ tile = this.getTile(i, j, create, evictGuard);
+ if (tile) {
+ visited[tile.toString()] = true;
+ fn.call(thisObj, tile);
+ }
+ }
+ }
+ },
+
+ holdTilesIntersectingRect: function holdTilesIntersectingRect(rect) {
+ this.forEachIntersectingRect(rect, false, this.holdTile, this);
+ },
+
+ releaseTilesIntersectingRect: function releaseTilesIntersectingRect(rect) {
+ this.forEachIntersectingRect(rect, false, this.releaseTile, this);
+ }
+
+};
+
+
+
+TileManager.Tile = function Tile(i, j) {
+ // canvas element is where we keep paint data from browser for this tile
+ this._canvas = document.createElementNS(kXHTMLNamespaceURI, "canvas");
+ this._canvas.setAttribute("width", String(kTileWidth));
+ this._canvas.setAttribute("height", String(kTileHeight));
+ this._canvas.setAttribute("moz-opaque", "true");
+ // this._canvas.style.border = "1px solid red";
+
+ this.init(i, j); // defines more properties, cf below
+};
+
+TileManager.Tile.prototype = {
+
+ // essentially, this is part of constructor code, but since we reuse tiles
+ // in the tile cache, this is here so that we can reinitialize tiles when we
+ // reuse them
+ init: function init(i, j) {
+ if (!this.boundRect)
+ this.boundRect = new wsRect(i * kTileWidth, j * kTileHeight, kTileWidth, kTileHeight);
+ else
+ this.boundRect.setRect(i * kTileWidth, j * kTileHeight, kTileWidth, kTileHeight);
+
+ // indices!
+ this.i = i;
+ this.j = j;
+
+ // flags true if we need to repaint our own local canvas
+ this._dirtyTileCanvas = false;
+
+ // keep a dirty rectangle (i.e. only part of the tile is dirty)
+ this._dirtyTileCanvasRect = null;
+
+ // flag used by TileManager to avoid re-appending tiles that have already
+ // been appended
+ this._appended = false;
+
+ // We keep tile objects around after their use for later reuse, so this
+ // flags true for an unused pooled tile. We don't actually care about
+ // this from within the Tile prototype, it is here for the cache to use.
+ this.free = true;
+ },
+
+ // viewport coordinates
+ get x() { return this.boundRect.left; },
+ get y() { return this.boundRect.top; },
+
+ // the actual canvas that holds the most recently rendered image of this
+ // canvas
+ getContentImage: function getContentImage() { return this._canvas; },
+
+ isDirty: function isDirty() { return this._dirtyTileCanvas; },
+
+ /**
+ * Mark this entire tile as dirty (i.e. the whole tile needs to be rendered
+ * on next render).
+ */
+ markDirty: function markDirty() { this.updateDirtyRegion(); },
+
+ unmarkDirty: function unmarkDirty() {
+ this._dirtyTileCanvasRect = null;
+ this._dirtyTileCanvas = false;
+ },
+
+ /**
+ * This will mark dirty at least everything in dirtyRect (which must be
+ * specified in canvas coordinates). If dirtyRect is not given then
+ * the entire tile is marked dirty.
+ */
+ updateDirtyRegion: function updateDirtyRegion(dirtyRect) {
+ if (!dirtyRect) {
+
+ if (!this._dirtyTileCanvasRect)
+ this._dirtyTileCanvasRect = this.boundRect.clone();
+ else
+ this._dirtyTileCanvasRect.copyFrom(this.boundRect);
+
+ } else if (!this._dirtyTileCanvasRect) {
+ this._dirtyTileCanvasRect = dirtyRect.intersect(this.boundRect);
+ } else if (dirtyRect.intersects(this.boundRect)) {
+ this._dirtyTileCanvasRect.expandToContain(dirtyRect.intersect(this.boundRect));
+ }
+
+ // TODO if after the above, the dirty rectangle is large enough,
+ // mark the whole tile dirty.
+
+ if (this._dirtyTileCanvasRect)
+ this._dirtyTileCanvas = true;
+ },
+
+ /**
+ * Actually draw the browser content into the dirty region of this
+ * tile. This requires us to actually draw with the
+ * nsIDOMCanvasRenderingContext2D object's drawWindow method, which
+ * we expect to be a relatively heavy operation.
+ *
+ * You likely want to check if the tile isDirty() before asking it
+ * to render, as this will cause the entire tile to re-render in the
+ * case that it is not dirty.
+ */
+ render: function render(browser, browserView) {
+ if (!this.isDirty())
+ this.markDirty();
+
+ let rect = this._dirtyTileCanvasRect;
+
+ let x = rect.left - this.boundRect.left;
+ let y = rect.top - this.boundRect.top;
+
+ // content process is not being scaled, so don't scale our rect either
+ // browserView.viewportToBrowserRect(rect);
+ // rect.round(); // snap outward to get whole "pixel" (in browser coords)
+
+ let ctx = this._canvas.getContext("2d");
+ ctx.save();
+
+ browserView.browserToViewportCanvasContext(ctx);
+
+ ctx.translate(x, y);
+
+ let cw = browserView._contentWindow;
+ // let cw = browser.contentWindow;
+ ctx.asyncDrawXULElement(browserView._browser,
+ rect.left, rect.top,
+ rect.right - rect.left, rect.bottom - rect.top,
+ "grey",
+ (ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_CARET));
+
+ ctx.restore();
+
+ this.unmarkDirty();
+ },
+
+ toString: function toString(more) {
+ if (more) {
+ return 'Tile(' + [this.i,
+ this.j,
+ "dirty=" + this.isDirty(),
+ "boundRect=" + this.boundRect].join(', ')
+ + ')';
+ }
+
+ return 'Tile(' + this.i + ', ' + this.j + ')';
+ },
+
+ _hold: function hold() { this.free = false; },
+ _release: function release() { this.free = true; }
+
+};
+
+
+/**
+ * A CrawlIterator is in charge of creating and returning subsequent tiles "crawled"
+ * over as we render tiles lazily. It supports iterator semantics so you can use
+ * CrawlIterator objects in for..in loops.
+ *
+ * Currently the CrawlIterator is built to expand a rectangle iteratively and return
+ * subsequent tiles that intersect the boundary of the rectangle. Each expansion of
+ * the rectangle is one unit of tile dimensions in each direction. This is repeated
+ * until all tiles from elsewhere have been reused (assuming the cache has finite
+ * capacity) in this crawl, so that we don't start reusing tiles from the beginning
+ * of our crawl. Afterward, the CrawlIterator enters a state where it operates as a
+ * FIFO queue, and calls to next() simply dequeue elements, which must be added with
+ * enqueue().
+ *
+ * @param tileCache The TileCache over whose tiles this CrawlIterator will crawl
+ * @param startRect [optional] The rectangle that we grow in the first (rectangle
+ * expansion) iteration state.
+ */
+TileManager.CrawlIterator = function CrawlIterator(tileCache, startRect) {
+ this._tileCache = tileCache;
+ this._stepRect = startRect;
+
+ // used to remember tiles that we've reused during this crawl
+ this._visited = {};
+
+ // filters the tiles we've already reused once from being considered victims
+ // for reuse when we ask the tile cache to create a new tile
+ let visited = this._visited;
+ this._notVisited = function(tile) { return !visited[tile]; };
+
+ // a generator that generates tile indices corresponding to tiles intersecting
+ // the boundary of an expanding rectangle
+ this._crawlIndices = !startRect ? null : (function indicesGenerator(rect, tc) {
+ let outOfBounds = false;
+ while (!outOfBounds) {
+ // expand rect
+ rect.left -= kTileWidth;
+ rect.right += kTileWidth;
+ rect.top -= kTileHeight;
+ rect.bottom += kTileHeight;
+
+ let dx = (rect.right % kTileWidth) - (rect.left % kTileWidth);
+ let dy = (rect.bottom % kTileHeight) - (rect.top % kTileHeight);
+
+ outOfBounds = true;
+
+ // top, bottom borders
+ for (let y of [rect.top, rect.bottom]) {
+ for (let x = rect.left; x <= rect.right - dx; x += kTileWidth) {
+ let i = x >> kTileExponentWidth;
+ let j = y >> kTileExponentHeight;
+ if (tc.inBounds(i, j)) {
+ outOfBounds = false;
+ yield [i, j];
+ }
+ }
+ }
+
+ // left, right borders
+ for (let x of [rect.left, rect.right]) {
+ for (let y = rect.top; y <= rect.bottom - dy; y += kTileHeight) {
+ let i = x >> kTileExponentWidth;
+ let j = y >> kTileExponentHeight;
+ if (tc.inBounds(i, j)) {
+ outOfBounds = false;
+ yield [i, j];
+ }
+ }
+ }
+ }
+ })(this._stepRect, this._tileCache), // instantiate the generator
+
+ // after we finish the rectangle iteration state, we enter the FIFO queue state
+ this._queueState = !startRect;
+ this._queue = [];
+
+ // used to prevent tiles from being enqueued twice --- "patience, we'll get to
+ // it in a moment"
+ this._enqueued = {};
+};
+
+TileManager.CrawlIterator.prototype = {
+ __iterator__: function*() {
+ while (true) {
+ let tile = this.next();
+ if (!tile) break;
+ yield tile;
+ }
+ },
+
+ becomeQueue: function becomeQueue() {
+ this._queueState = true;
+ },
+
+ unbecomeQueue: function unbecomeQueue() {
+ this._queueState = false;
+ },
+
+ next: function next() {
+ if (this._queueState)
+ return this.dequeue();
+
+ let tile = null;
+
+ if (this._crawlIndices) {
+ try {
+ let [i, j] = this._crawlIndices.next();
+ tile = this._tileCache.getTile(i, j, true, this._notVisited);
+ } catch (e) {
+ if (!(e instanceof StopIteration))
+ throw e;
+ }
+ }
+
+ if (tile) {
+ this._visited[tile] = true;
+ } else {
+ this.becomeQueue();
+ return this.next();
+ }
+
+ return tile;
+ },
+
+ dequeue: function dequeue() {
+ let tile = null;
+ do {
+ let idx = this._queue.shift();
+ if (!idx)
+ return null;
+
+ delete this._enqueued[idx];
+ let [i, j] = this._unstrIndices(idx);
+ tile = this._tileCache.getTile(i, j, false);
+
+ } while (!tile);
+
+ return tile;
+ },
+
+ enqueue: function enqueue(i, j) {
+ let idx = this._strIndices(i, j);
+ if (!this._enqueued[idx]) {
+ this._queue.push(idx);
+ this._enqueued[idx] = true;
+ }
+ },
+
+ _strIndices: function _strIndices(i, j) {
+ return i + "," + j;
+ },
+
+ _unstrIndices: function _unstrIndices(str) {
+ return str.split(',');
+ }
+
+};
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js
new file mode 100644
index 000000000..69288e725
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js
@@ -0,0 +1,1438 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+
+var gWsDoLog = false;
+var gWsLogDiv = null;
+
+function logbase() {
+ if (!gWsDoLog)
+ return;
+
+ if (gWsLogDiv == null && "console" in window) {
+ console.log.apply(console, arguments);
+ } else {
+ var s = "";
+ for (var i = 0; i < arguments.length; i++) {
+ s += arguments[i] + " ";
+ }
+ s += "\n";
+ if (gWsLogDiv) {
+ gWsLogDiv.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "br"));
+ gWsLogDiv.appendChild(document.createTextNode(s));
+ }
+
+ dump(s);
+ }
+}
+
+function dumpJSStack(stopAtNamedFunction) {
+ let caller = Components.stack.caller;
+ dump("\tStack: " + caller.name);
+ while ((caller = caller.caller)) {
+ dump(" <- " + caller.name);
+ if (stopAtNamedFunction && caller.name != "anonymous")
+ break;
+ }
+ dump("\n");
+}
+
+function log() {
+ // logbase.apply(window, arguments);
+}
+
+function log2() {
+ // logbase.apply(window, arguments);
+}
+
+var reportError = log;
+
+/*
+ * wsBorder class
+ *
+ * Simple container for top,left,bottom,right "border" values
+ */
+function wsBorder(t, l, b, r) {
+ this.setBorder(t, l, b, r);
+}
+
+wsBorder.prototype = {
+
+ setBorder: function (t, l, b, r) {
+ this.top = t;
+ this.left = l;
+ this.bottom = b;
+ this.right = r;
+ },
+
+ toString: function () {
+ return "[l:" + this.left + ",t:" + this.top + ",r:" + this.right + ",b:" + this.bottom + "]";
+ }
+};
+
+/*
+ * wsRect class
+ *
+ * Rectangle class, with both x/y/w/h and t/l/b/r accessors.
+ */
+function wsRect(x, y, w, h) {
+ this.left = x;
+ this.top = y;
+ this.right = x+w;
+ this.bottom = y+h;
+}
+
+wsRect.prototype = {
+
+ 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.left - v;
+ this.left = v;
+ this.right -= diff;
+ },
+ set y(v) {
+ let diff = this.top - v;
+ this.top = v;
+ this.bottom -= diff;
+ },
+ set width(v) { this.right = this.left + v; },
+ set height(v) { this.bottom = this.top + v; },
+
+ setRect: function(x, y, w, h) {
+ this.left = x;
+ this.top = y;
+ this.right = x+w;
+ this.bottom = y+h;
+
+ return this;
+ },
+
+ setBounds: function(t, l, b, r) {
+ this.top = t;
+ this.left = l;
+ this.bottom = b;
+ this.right = r;
+
+ return this;
+ },
+
+ 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.top = r.top;
+ this.left = r.left;
+ this.bottom = r.bottom;
+ this.right = r.right;
+
+ return this;
+ },
+
+ copyFromTLBR: function(r) {
+ this.left = r.left;
+ this.top = r.top;
+ this.right = r.right;
+ this.bottom = r.bottom;
+
+ return this;
+ },
+
+ translate: function(x, 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.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);
+ },
+
+ toString: function() {
+ return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]";
+ },
+
+ expandBy: function(b) {
+ this.left += b.left;
+ this.right += b.right;
+ this.top += b.top;
+ this.bottom += b.bottom;
+ return this;
+ },
+
+ contains: function(other) {
+ return !!(other.left >= this.left &&
+ other.right <= this.right &&
+ other.top >= this.top &&
+ other.bottom <= this.bottom);
+ },
+
+ intersect: function(r2) {
+ let xmost1 = this.right;
+ let xmost2 = r2.right;
+
+ let x = Math.max(this.left, r2.left);
+
+ let temp = Math.min(xmost1, xmost2);
+ if (temp <= x)
+ return null;
+
+ let width = temp - x;
+
+ let ymost1 = this.bottom;
+ let ymost2 = r2.bottom;
+ let y = Math.max(this.top, r2.top);
+
+ temp = Math.min(ymost1, ymost2);
+ if (temp <= y)
+ return null;
+
+ let height = temp - y;
+
+ return new wsRect(x, y, width, height);
+ },
+
+ intersects: function(other) {
+ 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;
+ },
+
+ /**
+ * Similar to (and most code stolen from) intersect(). A restriction
+ * is an intersection, but this modifies the receiving object instead
+ * of returning a new rect.
+ */
+ restrictTo: function restrictTo(r2) {
+ let xmost1 = this.right;
+ let xmost2 = r2.right;
+
+ let x = Math.max(this.left, r2.left);
+
+ let temp = Math.min(xmost1, xmost2);
+ if (temp <= x)
+ throw "Intersection is empty but rects cannot be empty";
+
+ let width = temp - x;
+
+ let ymost1 = this.bottom;
+ let ymost2 = r2.bottom;
+ let y = Math.max(this.top, r2.top);
+
+ temp = Math.min(ymost1, ymost2);
+ if (temp <= y)
+ throw "Intersection is empty but rects cannot be empty";
+
+ let height = temp - y;
+
+ return this.setRect(x, y, width, height);
+ },
+
+ /**
+ * Similar to (and most code stolen from) union(). An extension is a
+ * union (in our sense of the term, not the common set-theoretic sense),
+ * but this modifies the receiving object instead of returning a new rect.
+ * Effectively, this rectangle is expanded minimally to contain all of the
+ * other rect. "Expanded minimally" means that the rect may shrink if
+ * given a strict subset rect as the argument.
+ */
+ expandToContain: function extendTo(rect) {
+ 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);
+ },
+
+ round: function round(scale) {
+ if (!scale) scale = 1;
+
+ 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.left *= xscl;
+ this.right *= xscl;
+ this.top *= yscl;
+ this.bottom *= yscl;
+
+ return this;
+ }
+};
+
+/*
+ * The "Widget Stack"
+ *
+ * Manages a <xul:stack>'s children, allowing them to be dragged around
+ * the stack, subject to specified constraints. Optionally supports
+ * one widget designated as the viewport, which can be panned over a virtual
+ * area without needing to draw that area entirely. The viewport widget
+ * is designated by a 'viewport' attribute on the child element.
+ *
+ * Widgets are subject to various constraints, specified in xul via the
+ * 'constraint' attribute. Current constraints are:
+ * ignore-x: When panning, ignore any changes to the widget's x position
+ * ignore-y: When panning, ignore any changes to the widget's y position
+ * vp-relative: This widget's position should be claculated relative to
+ * the viewport widget. It will always keep the same offset from that
+ * widget as initially laid out, regardless of changes to the viewport
+ * bounds.
+ * frozen: This widget is in a fixed position and should never pan.
+ */
+function WidgetStack(el, ew, eh) {
+ this.init(el, ew, eh);
+}
+
+WidgetStack.prototype = {
+ // the <stack> element
+ _el: null,
+
+ // object indexed by widget id, with state struct for each object (see _addNewWidget)
+ _widgetState: null,
+
+ // any barriers
+ _barriers: null,
+
+ // If a viewport widget is present, this will point to its state object;
+ // otherwise null.
+ _viewport: null,
+
+ // a wsRect; the inner bounds of the viewport content
+ _viewportBounds: null,
+ // a wsBorder; the overflow area to the side of the bounds where our
+ // viewport-relative widgets go
+ _viewportOverflow: null,
+
+ // a wsRect; the viewportBounds expanded by the viewportOverflow
+ _pannableBounds: null,
+ get pannableBounds() {
+ if (!this._pannableBounds) {
+ this._pannableBounds = this._viewportBounds.clone()
+ .expandBy(this._viewportOverflow);
+ }
+ return this._pannableBounds.clone();
+ },
+
+ // a wsRect; the currently visible part of pannableBounds.
+ _viewingRect: null,
+
+ // the amount of current global offset applied to all widgets (whether
+ // static or not). Set via offsetAll(). Can be used to push things
+ // out of the way for overlaying some other UI.
+ globalOffsetX: 0,
+ globalOffsetY: 0,
+
+ // if true (default), panning is constrained to the pannable bounds.
+ _constrainToViewport: true,
+
+ _viewportUpdateInterval: -1,
+ _viewportUpdateTimeout: -1,
+
+ _viewportUpdateHandler: null,
+ _panHandler: null,
+
+ _dragState: null,
+
+ _skipViewportUpdates: 0,
+ _forceViewportUpdate: false,
+
+ //
+ // init:
+ // el: the <stack> element whose children are to be managed
+ //
+ init: function (el, ew, eh) {
+ this._el = el;
+ this._widgetState = {};
+ this._barriers = [];
+
+ let rect = this._el.getBoundingClientRect();
+ let width = rect.width;
+ let height = rect.height;
+
+ if (ew != undefined && eh != undefined) {
+ width = ew;
+ height = eh;
+ }
+
+ this._viewportOverflow = new wsBorder(0, 0, 0, 0);
+
+ this._viewingRect = new wsRect(0, 0, width, height);
+
+ // listen for DOMNodeInserted/DOMNodeRemoved/DOMAttrModified
+ let children = this._el.childNodes;
+ for (let i = 0; i < children.length; i++) {
+ let c = this._el.childNodes[i];
+ if (c.tagName == "spacer")
+ this._addNewBarrierFromSpacer(c);
+ else
+ this._addNewWidget(c);
+ }
+
+ // this also updates the viewportOverflow and pannableBounds
+ this._updateWidgets();
+
+ if (this._viewport) {
+ this._viewportBounds = new wsRect(0, 0, this._viewport.rect.width, this._viewport.rect.height);
+ } else {
+ this._viewportBounds = new wsRect(0, 0, 0, 0);
+ }
+ },
+
+ // moveWidgetBy: move the widget with the given id by x,y. Should
+ // not be used on vp-relative or otherwise frozen widgets (using it
+ // on the x coordinate for x-ignore widgets and similarily for y is
+ // ok, as long as the other coordinate remains 0.)
+ moveWidgetBy: function (wid, x, y) {
+ let state = this._getState(wid);
+
+ state.rect.x += x;
+ state.rect.y += y;
+
+ this._commitState(state);
+ },
+
+ // panBy: pan the entire set of widgets by the given x and y amounts.
+ // This does the same thing as if the user dragged by the given amount.
+ // If this is called with an outstanding drag, weirdness might happen,
+ // but it also might work, so not disabling that.
+ //
+ // if ignoreBarriers is true, then barriers are ignored for the pan.
+ panBy: function panBy(dx, dy, ignoreBarriers) {
+ dx = Math.round(dx);
+ dy = Math.round(dy);
+
+ if (dx == 0 && dy == 0)
+ return false;
+
+ let needsDragWrap = !this._dragging;
+
+ if (needsDragWrap)
+ this.dragStart(0, 0);
+
+ let panned = this._panBy(dx, dy, ignoreBarriers);
+
+ if (needsDragWrap)
+ this.dragStop();
+
+ return panned;
+ },
+
+ // panTo: pan the entire set of widgets so that the given x,y
+ // coordinates are in the upper left of the stack. If either is
+ // null or undefined, only move the other axis
+ panTo: function panTo(x, y) {
+ if (x == undefined || x == null)
+ x = this._viewingRect.x;
+ if (y == undefined || y == null)
+ y = this._viewingRect.y;
+ this.panBy(x - this._viewingRect.x, y - this._viewingRect.y, true);
+ },
+
+ // freeze: set a widget as frozen. A frozen widget won't be moved
+ // in the stack -- its x,y position will still be tracked in the
+ // state, but the left/top attributes won't be overwritten. Call unfreeze
+ // to move the widget back to where the ws thinks it should be.
+ freeze: function (wid) {
+ let state = this._getState(wid);
+
+ state.frozen = true;
+ },
+
+ unfreeze: function (wid) {
+ let state = this._getState(wid);
+ if (!state.frozen)
+ return;
+
+ state.frozen = false;
+ this._commitState(state);
+ },
+
+ // moveFrozenTo: move a frozen widget with id wid to x, y in the stack.
+ // can only be used on frozen widgets
+ moveFrozenTo: function (wid, x, y) {
+ let state = this._getState(wid);
+ if (!state.frozen)
+ throw "moveFrozenTo on non-frozen widget " + wid;
+
+ state.widget.setAttribute("left", x);
+ state.widget.setAttribute("top", y);
+ },
+
+ // moveUnfrozenTo: move an unfrozen, pannable widget with id wid to x, y in
+ // the stack. should only be used on unfrozen widgets when a dynamic change
+ // in position needs to be made. we basically remove, adjust and re-add
+ // the widget
+ moveUnfrozenTo: function (wid, x, y) {
+ delete this._widgetState[wid];
+ let widget = document.getElementById(wid);
+ if (x) widget.setAttribute("left", x);
+ if (y) widget.setAttribute("top", y);
+ this._addNewWidget(widget);
+ this._updateWidgets();
+ },
+
+ // we're relying on viewportBounds and viewingRect having the same origin
+ get viewportVisibleRect () {
+ let rect = this._viewportBounds.intersect(this._viewingRect);
+ if (!rect)
+ rect = new wsRect(0, 0, 0, 0);
+ return rect;
+ },
+
+ isWidgetFrozen: function isWidgetFrozen(wid) {
+ return this._getState(wid).frozen;
+ },
+
+ // isWidgetVisible: return true if any portion of widget with id wid is
+ // visible; otherwise return false.
+ isWidgetVisible: function (wid) {
+ let state = this._getState(wid);
+ let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
+
+ return visibleStackRect.intersects(state.rect);
+ },
+
+ // getWidgetVisibility: returns the percentage that the widget is visible
+ getWidgetVisibility: function (wid) {
+ let state = this._getState(wid);
+ let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
+
+ let visibleRect = visibleStackRect.intersect(state.rect);
+ if (visibleRect)
+ return [visibleRect.width / state.rect.width, visibleRect.height / state.rect.height]
+
+ return [0, 0];
+ },
+
+ // offsetAll: add an offset to all widgets
+ offsetAll: function (x, y) {
+ this.globalOffsetX += x;
+ this.globalOffsetY += y;
+
+ for (let wid in this._widgetState) {
+ let state = this._widgetState[wid];
+ state.rect.x += x;
+ state.rect.y += y;
+
+ this._commitState(state);
+ }
+ },
+
+ // setViewportBounds
+ // nb: an object containing top, left, bottom, right properties
+ // OR
+ // width, height: integer values; origin assumed to be 0,0
+ // OR
+ // top, left, bottom, right: integer values
+ //
+ // Set the bounds of the viewport area; that is, set the size of the
+ // actual content that the viewport widget will be providing a view
+ // over. For example, in the case of a 100x100 viewport showing a
+ // view into a 100x500 webpage, the viewport bounds would be
+ // { top: 0, left: 0, bottom: 500, right: 100 }.
+ //
+ // setViewportBounds will move all the viewport-relative widgets into
+ // place based on the new viewport bounds.
+ setViewportBounds: function setViewportBounds() {
+ let oldBounds = this._viewportBounds.clone();
+
+ if (arguments.length == 1) {
+ this._viewportBounds.copyFromTLBR(arguments[0]);
+ } else if (arguments.length == 2) {
+ this._viewportBounds.setRect(0, 0, arguments[0], arguments[1]);
+ } else if (arguments.length == 4) {
+ this._viewportBounds.setBounds(arguments[0],
+ arguments[1],
+ arguments[2],
+ arguments[3]);
+ } else {
+ throw "Invalid number of arguments to setViewportBounds";
+ }
+
+ let vp = this._viewport;
+
+ let dleft = this._viewportBounds.left - oldBounds.left;
+ let dright = this._viewportBounds.right - oldBounds.right;
+ let dtop = this._viewportBounds.top - oldBounds.top;
+ let dbottom = this._viewportBounds.bottom - oldBounds.bottom;
+
+ // log2("setViewportBounds dltrb", dleft, dtop, dright, dbottom);
+
+ // move all vp-relative widgets to be the right offset from the bounds again
+ for (let wid in this._widgetState) {
+ let state = this._widgetState[wid];
+ if (state.vpRelative) {
+ // log2("vpRelative widget", state.id, state.rect.x, dleft, dright);
+ if (state.vpOffsetXBefore) {
+ state.rect.x += dleft;
+ } else {
+ state.rect.x += dright;
+ }
+
+ if (state.vpOffsetYBefore) {
+ state.rect.y += dtop;
+ } else {
+ state.rect.y += dbottom;
+ }
+
+ // log2("vpRelative widget", state.id, state.rect.x, dleft, dright);
+ this._commitState(state);
+ }
+ }
+
+ for (let bid in this._barriers) {
+ let barrier = this._barriers[bid];
+
+ // log2("setViewportBounds: looking at barrier", bid, barrier.vpRelative, barrier.type);
+
+ if (barrier.vpRelative) {
+ if (barrier.type == "vertical") {
+ let q = "v barrier moving from " + barrier.x + " to ";
+ if (barrier.vpOffsetXBefore) {
+ barrier.x += dleft;
+ } else {
+ barrier.x += dright;
+ }
+ // log2(q += barrier.x);
+ } else if (barrier.type == "horizontal") {
+ let q = "h barrier moving from " + barrier.y + " to ";
+ if (barrier.vpOffsetYBefore) {
+ barrier.y += dtop;
+ } else {
+ barrier.y += dbottom;
+ }
+ // log2(q += barrier.y);
+ }
+ }
+ }
+
+ // clear the pannable bounds cache to make sure it gets rebuilt
+ this._pannableBounds = null;
+
+ // now let's make sure that the viewing rect and inner bounds are still valid
+ this._adjustViewingRect();
+
+ this._viewportUpdate(0, 0, true);
+ },
+
+ // setViewportHandler
+ // uh: A function object
+ //
+ // The given function object is called at the end of every drag and viewport
+ // bounds change, passing in the new rect that's to be displayed in the
+ // viewport.
+ //
+ setViewportHandler: function (uh) {
+ this._viewportUpdateHandler = uh;
+ },
+
+ // setPanHandler
+ // uh: A function object
+ //
+ // The given functin object is called whenever elements pan; it provides
+ // the new area of the pannable bounds that's visible in the stack.
+ setPanHandler: function (uh) {
+ this._panHandler = uh;
+ },
+
+ // dragStart: start a drag, with the current coordinates being clientX,clientY
+ dragStart: function dragStart(clientX, clientY) {
+ log("(dragStart)", clientX, clientY);
+
+ if (this._dragState) {
+ reportError("dragStart with drag already in progress? what?");
+ this._dragState = null;
+ }
+
+ this._dragState = { };
+
+ let t = Date.now();
+
+ this._dragState.barrierState = [];
+
+ this._dragState.startTime = t;
+ // outer x, that is outer from the viewport coordinates. In stack-relative coords.
+ this._dragState.outerStartX = clientX;
+ this._dragState.outerStartY = clientY;
+
+ this._dragCoordsFromClient(clientX, clientY, t);
+
+ this._dragState.outerLastUpdateDX = 0;
+ this._dragState.outerLastUpdateDY = 0;
+
+ if (this._viewport) {
+ // create a copy of these so that we can compute
+ // deltas correctly to update the viewport
+ this._viewport.dragStartRect = this._viewport.rect.clone();
+ }
+
+ this._dragState.dragging = true;
+ },
+
+ _viewportDragUpdate: function viewportDragUpdate() {
+ let vws = this._viewport;
+ this._viewportUpdate((vws.dragStartRect.x - vws.rect.x),
+ (vws.dragStartRect.y - vws.rect.y));
+ },
+
+ // dragStop: stop any drag in progress
+ dragStop: function dragStop() {
+ log("(dragStop)");
+
+ if (!this._dragging)
+ return;
+
+ if (this._viewportUpdateTimeout != -1)
+ clearTimeout(this._viewportUpdateTimeout);
+
+ this._viewportDragUpdate();
+
+ this._dragState = null;
+ },
+
+ // dragMove: process a mouse move to clientX,clientY for an ongoing drag
+ dragMove: function dragMove(clientX, clientY) {
+ if (!this._dragging)
+ return false;
+
+ this._dragCoordsFromClient(clientX, clientY);
+
+ let panned = this._dragUpdate();
+
+ if (this._viewportUpdateInterval != -1) {
+ if (this._viewportUpdateTimeout != -1)
+ clearTimeout(this._viewportUpdateTimeout);
+ let self = this;
+ this._viewportUpdateTimeout = setTimeout(function () { self._viewportDragUpdate(); }, this._viewportUpdateInterval);
+ }
+
+ return panned;
+ },
+
+ // dragBy: process a mouse move by dx,dy for an ongoing drag
+ dragBy: function dragBy(dx, dy) {
+ return this.dragMove(this._dragState.outerCurX + dx, this._dragState.outerCurY + dy);
+ },
+
+ // updateSize: tell the WidgetStack to update its size, because it
+ // was either resized or some other event took place.
+ updateSize: function updateSize(width, height) {
+ if (width == undefined || height == undefined) {
+ let rect = this._el.getBoundingClientRect();
+ width = rect.width;
+ height = rect.height;
+ }
+
+ // update widget rects and viewportOverflow, since the resize might have
+ // caused them to change (widgets first, since the viewportOverflow depends
+ // on them).
+
+ // XXX these methods aren't working correctly yet, but they aren't strictly
+ // necessary in Fennec's default config
+ // for (let wid in this._widgetState) {
+ // let s = this._widgetState[wid];
+ // this._updateWidgetRect(s);
+ // }
+ // this._updateViewportOverflow();
+
+ this._viewingRect.width = width;
+ this._viewingRect.height = height;
+
+ // Wrap this call in a batch to ensure that we always call the
+ // viewportUpdateHandler, even if _adjustViewingRect doesn't trigger a pan.
+ // If it does, the batch also ensures that we don't call the handler twice.
+ this.beginUpdateBatch();
+ this._adjustViewingRect();
+ this.endUpdateBatch();
+ },
+
+ beginUpdateBatch: function startUpdate() {
+ if (!this._skipViewportUpdates) {
+ this._startViewportBoundsString = this._viewportBounds.toString();
+ this._forceViewportUpdate = false;
+ }
+ this._skipViewportUpdates++;
+ },
+
+ endUpdateBatch: function endUpdate(aForceRedraw) {
+ if (!this._skipViewportUpdates)
+ throw new Error("Unbalanced call to endUpdateBatch");
+
+ this._forceViewportUpdate = this._forceViewportUpdate || aForceRedraw;
+
+ this._skipViewportUpdates--;
+ if (this._skipViewportUpdates)
+ return;
+
+ let boundsSizeChanged =
+ this._startViewportBoundsString != this._viewportBounds.toString();
+ this._callViewportUpdateHandler(boundsSizeChanged || this._forceViewportUpdate);
+ },
+
+ //
+ // Internal code
+ //
+
+ _updateWidgetRect: function(state) {
+ // don't need to support updating the viewport rect at the moment
+ // (we'd need to duplicate the vptarget* code from _addNewWidget if we did)
+ if (state == this._viewport)
+ return;
+
+ let w = state.widget;
+ let x = w.getAttribute("left") || 0;
+ let y = w.getAttribute("top") || 0;
+ let rect = w.getBoundingClientRect();
+ state.rect = new wsRect(parseInt(x), parseInt(y),
+ rect.right - rect.left,
+ rect.bottom - rect.top);
+ if (w.hasAttribute("widgetwidth") && w.hasAttribute("widgetheight")) {
+ state.rect.width = parseInt(w.getAttribute("widgetwidth"));
+ state.rect.height = parseInt(w.getAttribute("widgetheight"));
+ }
+ },
+
+ _dumpRects: function () {
+ dump("WidgetStack:\n");
+ dump("\tthis._viewportBounds: " + this._viewportBounds + "\n");
+ dump("\tthis._viewingRect: " + this._viewingRect + "\n");
+ dump("\tthis._viewport.viewportInnerBounds: " + this._viewport.viewportInnerBounds + "\n");
+ dump("\tthis._viewport.rect: " + this._viewport.rect + "\n");
+ dump("\tthis._viewportOverflow: " + this._viewportOverflow + "\n");
+ dump("\tthis.pannableBounds: " + this.pannableBounds + "\n");
+ },
+
+ // Ensures that _viewingRect is within _pannableBounds (call this when either
+ // one is resized)
+ _adjustViewingRect: function _adjustViewingRect() {
+ let vr = this._viewingRect;
+ let pb = this.pannableBounds;
+
+ if (pb.contains(vr))
+ return; // nothing to do here
+
+ // don't bother adjusting _viewingRect if it can't fit into
+ // _pannableBounds
+ if (vr.height > pb.height || vr.width > pb.width)
+ return;
+
+ let panX = 0, panY = 0;
+ if (vr.right > pb.right)
+ panX = pb.right - vr.right;
+ else if (vr.left < pb.left)
+ panX = pb.left - vr.left;
+
+ if (vr.bottom > pb.bottom)
+ panY = pb.bottom - vr.bottom;
+ else if (vr.top < pb.top)
+ panY = pb.top - vr.top;
+
+ this.panBy(panX, panY, true);
+ },
+
+ _getState: function (wid) {
+ let w = this._widgetState[wid];
+ if (!w)
+ throw "Unknown widget id '" + wid + "'; widget not in stack";
+ return w;
+ },
+
+ get _dragging() {
+ return this._dragState && this._dragState.dragging;
+ },
+
+ _viewportUpdate: function _viewportUpdate(dX, dY, boundsChanged) {
+ if (!this._viewport)
+ return;
+
+ this._viewportUpdateTimeout = -1;
+
+ let vws = this._viewport;
+ let vwib = vws.viewportInnerBounds;
+ let vpb = this._viewportBounds;
+
+ // recover the amount the inner bounds moved by the amount the viewport
+ // widget moved, but don't include offsets that we're making up from previous
+ // drags that didn't affect viewportInnerBounds
+ let [ignoreX, ignoreY] = this._offsets || [0, 0];
+ let rx = dX - ignoreX;
+ let ry = dY - ignoreY;
+
+ [dX, dY] = this._rectTranslateConstrain(rx, ry, vwib, vpb);
+
+ // record the offsets that correspond to the amount of the drag we're ignoring
+ // to ensure the viewportInnerBounds remains within the viewportBounds
+ this._offsets = [dX - rx, dY - ry];
+
+ // adjust the viewportInnerBounds, and snap the viewport back
+ vwib.translate(dX, dY);
+ vws.rect.translate(dX, dY);
+ this._commitState(vws);
+
+ // update this so that we can call this function again during the same drag
+ // and get the right values.
+ vws.dragStartRect = vws.rect.clone();
+
+ this._callViewportUpdateHandler(boundsChanged);
+ },
+
+ _callViewportUpdateHandler: function _callViewportUpdateHandler(boundsChanged) {
+ if (!this._viewport || !this._viewportUpdateHandler || this._skipViewportUpdates)
+ return;
+
+ let vwb = this._viewportBounds.clone();
+
+ let vwib = this._viewport.viewportInnerBounds.clone();
+
+ let vis = this.viewportVisibleRect;
+
+ vwib.left += this._viewport.offsetLeft;
+ vwib.top += this._viewport.offsetTop;
+ vwib.right += this._viewport.offsetRight;
+ vwib.bottom += this._viewport.offsetBottom;
+
+ this._viewportUpdateHandler.apply(window, [vwb, vwib, vis, boundsChanged]);
+ },
+
+ _dragCoordsFromClient: function (cx, cy, t) {
+ this._dragState.curTime = t ? t : Date.now();
+ this._dragState.outerCurX = cx;
+ this._dragState.outerCurY = cy;
+
+ let dx = this._dragState.outerCurX - this._dragState.outerStartX;
+ let dy = this._dragState.outerCurY - this._dragState.outerStartY;
+ this._dragState.outerDX = dx;
+ this._dragState.outerDY = dy;
+ },
+
+ _panHandleBarriers: function (dx, dy) {
+ // XXX unless the barriers are sorted by position, this will break
+ // with multiple barriers that are near enough to eachother that a
+ // drag could cross more than one.
+
+ let vr = this._viewingRect;
+
+ // XXX this just stops at the first horizontal and vertical barrier it finds
+
+ // barrier_[xy] is the barrier that was used to get to the final
+ // barrier_d[xy] value. if null, no barrier, and dx/dy shouldn't
+ // be replaced with barrier_d[xy].
+ let barrier_y = null, barrier_x = null;
+ let barrier_dy = 0, barrier_dx = 0;
+
+ for (let i = 0; i < this._barriers.length; i++) {
+ let b = this._barriers[i];
+
+ // log2("barrier", i, b.type, b.x, b.y);
+
+ if (dx != 0 && b.type == "vertical") {
+ if (barrier_x != null) {
+ delete this._dragState.barrierState[i];
+ continue;
+ }
+
+ let alreadyKnownDistance = this._dragState.barrierState[i] || 0;
+
+ // log2("alreadyKnownDistance", alreadyKnownDistance);
+
+ let dbx = 0;
+
+ // 100 <= 100 && 100-(-5) > 100
+
+ if ((vr.left <= b.x && vr.left+dx > b.x) ||
+ (vr.left >= b.x && vr.left+dx < b.x))
+ {
+ dbx = b.x - vr.left;
+ } else if ((vr.right <= b.x && vr.right+dx > b.x) ||
+ (vr.right >= b.x && vr.right+dx < b.x))
+ {
+ dbx = b.x - vr.right;
+ } else {
+ delete this._dragState.barrierState[i];
+ continue;
+ }
+
+ let leftoverDistance = dbx - dx;
+
+ // log2("initial dbx", dbx, leftoverDistance);
+
+ let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size;
+
+ if (dist >= 0) {
+ if (dx < 0)
+ dbx -= dist;
+ else
+ dbx += dist;
+ delete this._dragState.barrierState[i];
+ } else {
+ dbx = 0;
+ this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance;
+ }
+
+ // log2("final dbx", dbx, "state", this._dragState.barrierState[i]);
+
+ if (Math.abs(barrier_dx) <= Math.abs(dbx)) {
+ barrier_x = b;
+ barrier_dx = dbx;
+
+ // log2("new barrier_dx", barrier_dx);
+ }
+ }
+
+ if (dy != 0 && b.type == "horizontal") {
+ if (barrier_y != null) {
+ delete this._dragState.barrierState[i];
+ continue;
+ }
+
+ let alreadyKnownDistance = this._dragState.barrierState[i] || 0;
+
+ // log2("alreadyKnownDistance", alreadyKnownDistance);
+
+ let dby = 0;
+
+ // 100 <= 100 && 100-(-5) > 100
+
+ if ((vr.top <= b.y && vr.top+dy > b.y) ||
+ (vr.top >= b.y && vr.top+dy < b.y))
+ {
+ dby = b.y - vr.top;
+ } else if ((vr.bottom <= b.y && vr.bottom+dy > b.y) ||
+ (vr.bottom >= b.y && vr.bottom+dy < b.y))
+ {
+ dby = b.y - vr.bottom;
+ } else {
+ delete this._dragState.barrierState[i];
+ continue;
+ }
+
+ let leftoverDistance = dby - dy;
+
+ // log2("initial dby", dby, leftoverDistance);
+
+ let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size;
+
+ if (dist >= 0) {
+ if (dy < 0)
+ dby -= dist;
+ else
+ dby += dist;
+ delete this._dragState.barrierState[i];
+ } else {
+ dby = 0;
+ this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance;
+ }
+
+ // log2("final dby", dby, "state", this._dragState.barrierState[i]);
+
+ if (Math.abs(barrier_dy) <= Math.abs(dby)) {
+ barrier_y = b;
+ barrier_dy = dby;
+
+ // log2("new barrier_dy", barrier_dy);
+ }
+ }
+ }
+
+ if (barrier_x) {
+ // log2("did barrier_x", barrier_x, "barrier_dx", barrier_dx);
+ dx = barrier_dx;
+ }
+
+ if (barrier_y) {
+ dy = barrier_dy;
+ }
+
+ return [dx, dy];
+ },
+
+ _panBy: function _panBy(dx, dy, ignoreBarriers) {
+ let vr = this._viewingRect;
+
+ // check if any barriers would be crossed by this pan, and take them
+ // into account. do this first.
+ if (!ignoreBarriers)
+ [dx, dy] = this._panHandleBarriers(dx, dy);
+
+ // constrain the full drag of the viewingRect to the pannableBounds.
+ // note that the viewingRect needs to move in the opposite
+ // direction of the pan, so we fiddle with the signs here (as you
+ // pan to the upper left, more of the bottom right becomes visible,
+ // so the viewing rect moves to the bottom right of the virtual surface).
+ [dx, dy] = this._rectTranslateConstrain(dx, dy, vr, this.pannableBounds);
+
+ // If the net result is that we don't have any room to move, then
+ // just return.
+ if (dx == 0 && dy == 0)
+ return false;
+
+ // the viewingRect moves opposite of the actual pan direction, see above
+ vr.x += dx;
+ vr.y += dy;
+
+ // Go through each widget and move it by dx,dy. Frozen widgets
+ // will be ignored in commitState.
+ // The widget rects are in real stack space though, so we need to subtract
+ // our (now negated) dx, dy from their coordinates.
+ for (let wid in this._widgetState) {
+ let state = this._widgetState[wid];
+ if (!state.ignoreX)
+ state.rect.x -= dx;
+ if (!state.ignoreY)
+ state.rect.y -= dy;
+
+ this._commitState(state);
+ }
+
+ /* Do not call panhandler during pans within a transaction.
+ * Those pans always end-up covering up the checkerboard and
+ * do not require sliding out the location bar
+ */
+ if (!this._skipViewportUpdates && this._panHandler)
+ this._panHandler.apply(window, [vr.clone(), dx, dy]);
+
+ return true;
+ },
+
+ _dragUpdate: function _dragUpdate() {
+ let dx = this._dragState.outerLastUpdateDX - this._dragState.outerDX;
+ let dy = this._dragState.outerLastUpdateDY - this._dragState.outerDY;
+
+ this._dragState.outerLastUpdateDX = this._dragState.outerDX;
+ this._dragState.outerLastUpdateDY = this._dragState.outerDY;
+
+ return this.panBy(dx, dy);
+ },
+
+ //
+ // widget addition/removal
+ //
+ _addNewWidget: function (w) {
+ let wid = w.getAttribute("id");
+ if (!wid) {
+ reportError("WidgetStack: child widget without id!");
+ return;
+ }
+
+ if (w.getAttribute("hidden") == "true")
+ return;
+
+ let state = {
+ widget: w,
+ id: wid,
+
+ viewport: false,
+ ignoreX: false,
+ ignoreY: false,
+ sticky: false,
+ frozen: false,
+ vpRelative: false,
+
+ offsetLeft: 0,
+ offsetTop: 0,
+ offsetRight: 0,
+ offsetBottom: 0
+ };
+
+ this._updateWidgetRect(state);
+
+ if (w.hasAttribute("constraint")) {
+ let cs = w.getAttribute("constraint").split(",");
+ for (let s of cs) {
+ if (s == "ignore-x")
+ state.ignoreX = true;
+ else if (s == "ignore-y")
+ state.ignoreY = true;
+ else if (s == "sticky")
+ state.sticky = true;
+ else if (s == "frozen") {
+ state.frozen = true;
+ } else if (s == "vp-relative")
+ state.vpRelative = true;
+ }
+ }
+
+ if (w.hasAttribute("viewport")) {
+ if (this._viewport)
+ reportError("WidgetStack: more than one viewport canvas in stack!");
+
+ this._viewport = state;
+ state.viewport = true;
+
+ if (w.hasAttribute("vptargetx") && w.hasAttribute("vptargety") &&
+ w.hasAttribute("vptargetw") && w.hasAttribute("vptargeth"))
+ {
+ let wx = parseInt(w.getAttribute("vptargetx"));
+ let wy = parseInt(w.getAttribute("vptargety"));
+ let ww = parseInt(w.getAttribute("vptargetw"));
+ let wh = parseInt(w.getAttribute("vptargeth"));
+
+ state.offsetLeft = state.rect.left - wx;
+ state.offsetTop = state.rect.top - wy;
+ state.offsetRight = state.rect.right - (wx + ww);
+ state.offsetBottom = state.rect.bottom - (wy + wh);
+
+ state.rect = new wsRect(wx, wy, ww, wh);
+ }
+
+ // initialize inner bounds to top-left
+ state.viewportInnerBounds = new wsRect(0, 0, state.rect.width, state.rect.height);
+ }
+
+ this._widgetState[wid] = state;
+
+ log ("(New widget: " + wid + (state.viewport ? " [viewport]" : "") + " at: " + state.rect + ")");
+ },
+
+ _removeWidget: function (w) {
+ let wid = w.getAttribute("id");
+ delete this._widgetState[wid];
+ this._updateWidgets();
+ },
+
+ // updateWidgets:
+ // Go through all the widgets and figure out their viewport-relative offsets.
+ // If the widget goes to the left or above the viewport widget, then
+ // vpOffsetXBefore or vpOffsetYBefore is set.
+ // See setViewportBounds for use of vpOffset* state variables, and for how
+ // the actual x and y coords of each widget are calculated based on their offsets
+ // and the viewport bounds.
+ _updateWidgets: function () {
+ let vp = this._viewport;
+
+ let ofRect = this._viewingRect.clone();
+
+ for (let wid in this._widgetState) {
+ let state = this._widgetState[wid];
+ if (vp && state.vpRelative) {
+ // compute the vpOffset from 0,0 assuming that the viewport rect is 0,0
+ if (state.rect.left >= vp.rect.right) {
+ state.vpOffsetXBefore = false;
+ state.vpOffsetX = state.rect.left - vp.rect.width;
+ } else {
+ state.vpOffsetXBefore = true;
+ state.vpOffsetX = state.rect.left - vp.rect.left;
+ }
+
+ if (state.rect.top >= vp.rect.bottom) {
+ state.vpOffsetYBefore = false;
+ state.vpOffsetY = state.rect.top - vp.rect.height;
+ } else {
+ state.vpOffsetYBefore = true;
+ state.vpOffsetY = state.rect.top - vp.rect.top;
+ }
+
+ log("widget", state.id, "offset", state.vpOffsetX, state.vpOffsetXBefore ? "b" : "a", state.vpOffsetY, state.vpOffsetYBefore ? "b" : "a", "rect", state.rect);
+ }
+ }
+
+ this._updateViewportOverflow();
+ },
+
+ // updates the viewportOverflow/pannableBounds
+ _updateViewportOverflow: function() {
+ let vp = this._viewport;
+ if (!vp)
+ return;
+
+ let ofRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
+
+ for (let wid in this._widgetState) {
+ let state = this._widgetState[wid];
+ if (vp && state.vpRelative) {
+ ofRect.left = Math.min(ofRect.left, state.rect.left);
+ ofRect.top = Math.min(ofRect.top, state.rect.top);
+ ofRect.right = Math.max(ofRect.right, state.rect.right);
+ ofRect.bottom = Math.max(ofRect.bottom, state.rect.bottom);
+ }
+ }
+
+ // prevent the viewportOverflow from having positive top/left or negative
+ // bottom/right values, which would otherwise happen if there aren't widgets
+ // beyond each of those edges
+ this._viewportOverflow = new wsBorder(
+ /* top*/ Math.round(Math.min(ofRect.top, 0)),
+ /* left*/ Math.round(Math.min(ofRect.left, 0)),
+ /* bottom*/ Math.round(Math.max(ofRect.bottom - vp.rect.height, 0)),
+ /* right*/ Math.round(Math.max(ofRect.right - vp.rect.width, 0))
+ );
+
+ // clear the _pannableBounds cache, since it depends on the
+ // viewportOverflow
+ this._pannableBounds = null;
+ },
+
+ _widgetBounds: function () {
+ let r = new wsRect(0, 0, 0, 0);
+
+ for (let wid in this._widgetState) {
+ let state = this._widgetState[wid];
+ r = r.union(state.rect);
+ }
+
+ return r;
+ },
+
+ _commitState: function (state) {
+ // if the widget is frozen, don't actually update its left/top;
+ // presumably the caller is managing those directly for now.
+ if (state.frozen)
+ return;
+ let w = state.widget;
+ let l = state.rect.x + state.offsetLeft;
+ let t = state.rect.y + state.offsetTop;
+
+ // cache left/top to avoid calling setAttribute unnessesarily
+ if (state._left != l) {
+ state._left = l;
+ w.setAttribute("left", l);
+ }
+
+ if (state._top != t) {
+ state._top = t;
+ w.setAttribute("top", t);
+ }
+ },
+
+ // constrain translate of rect by dx dy to bounds; return dx dy that can
+ // be used to bring rect up to the edge of bounds if we'd go over.
+ _rectTranslateConstrain: function (dx, dy, rect, bounds) {
+ let newX, newY;
+
+ // If the rect is larger than the bounds, allow it to increase its overlap
+ let woverflow = rect.width > bounds.width;
+ let hoverflow = rect.height > bounds.height;
+ if (woverflow || hoverflow) {
+ let intersection = rect.intersect(bounds);
+ let newIntersection = rect.clone().translate(dx, dy).intersect(bounds);
+ if (woverflow)
+ newX = (newIntersection.width > intersection.width) ? rect.x + dx : rect.x;
+ if (hoverflow)
+ newY = (newIntersection.height > intersection.height) ? rect.y + dy : rect.y;
+ }
+
+ // Common case, rect fits within the bounds
+ // clamp new X to within [bounds.left, bounds.right - rect.width],
+ // new Y to within [bounds.top, bounds.bottom - rect.height]
+ if (isNaN(newX))
+ newX = Math.min(Math.max(bounds.left, rect.x + dx), bounds.right - rect.width);
+ if (isNaN(newY))
+ newY = Math.min(Math.max(bounds.top, rect.y + dy), bounds.bottom - rect.height);
+
+ return [newX - rect.x, newY - rect.y];
+ },
+
+ // add a new barrier from a <spacer>
+ _addNewBarrierFromSpacer: function (el) {
+ let t = el.getAttribute("barriertype");
+
+ // XXX implement these at some point
+ // t != "lr" && t != "rl" &&
+ // t != "tb" && t != "bt" &&
+
+ if (t != "horizontal" &&
+ t != "vertical")
+ {
+ throw "Invalid barrier type: " + t;
+ }
+
+ let x, y;
+
+ let barrier = {};
+ let vp = this._viewport;
+
+ barrier.type = t;
+
+ if (el.getAttribute("left"))
+ barrier.x = parseInt(el.getAttribute("left"));
+ else if (el.getAttribute("top"))
+ barrier.y = parseInt(el.getAttribute("top"));
+ else
+ throw "Barrier without top or left attribute";
+
+ if (el.getAttribute("size"))
+ barrier.size = parseInt(el.getAttribute("size"));
+ else
+ barrier.size = 10;
+
+ if (el.hasAttribute("constraint")) {
+ let cs = el.getAttribute("constraint").split(",");
+ for (let s of cs) {
+ if (s == "ignore-x")
+ barrier.ignoreX = true;
+ else if (s == "ignore-y")
+ barrier.ignoreY = true;
+ else if (s == "sticky")
+ barrier.sticky = true;
+ else if (s == "frozen") {
+ barrier.frozen = true;
+ } else if (s == "vp-relative")
+ barrier.vpRelative = true;
+ }
+ }
+
+ if (barrier.vpRelative) {
+ if (barrier.type == "vertical") {
+ if (barrier.x >= vp.rect.right) {
+ barrier.vpOffsetXBefore = false;
+ barrier.vpOffsetX = barrier.x - vp.rect.right;
+ } else {
+ barrier.vpOffsetXBefore = true;
+ barrier.vpOffsetX = barrier.x - vp.rect.left;
+ }
+ } else if (barrier.type == "horizontal") {
+ if (barrier.y >= vp.rect.bottom) {
+ barrier.vpOffsetYBefore = false;
+ barrier.vpOffsetY = barrier.y - vp.rect.bottom;
+ } else {
+ barrier.vpOffsetYBefore = true;
+ barrier.vpOffsetY = barrier.y - vp.rect.top;
+ }
+
+ // log2("h barrier relative", barrier.vpOffsetYBefore, barrier.vpOffsetY);
+ }
+ }
+
+ this._barriers.push(barrier);
+ }
+};
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/firefoxOverlay.xul b/toolkit/content/tests/fennec-tile-testapp/chrome/content/firefoxOverlay.xul
new file mode 100644
index 000000000..612f8bb9f
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/firefoxOverlay.xul
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?xml-stylesheet href="chrome://tile/skin/overlay.css" type="text/css"?>
+<!DOCTYPE overlay SYSTEM "chrome://tile/locale/tile.dtd">
+<overlay id="tile-overlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <script src="overlay.js"/>
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="tile-strings" src="chrome://tile/locale/tile.properties"/>
+ </stringbundleset>
+
+ <menupopup id="menu_ToolsPopup">
+ <menuitem id="tile-hello" label="&tile.label;"
+ oncommand="tile.onMenuItemCommand(event);"/>
+ </menupopup>
+</overlay>
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/foo.xul b/toolkit/content/tests/fennec-tile-testapp/chrome/content/foo.xul
new file mode 100644
index 000000000..cdc01658a
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/foo.xul
@@ -0,0 +1,460 @@
+<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/javascript" src="WidgetStack.js"/>
+<script type="application/javascript" src="TileManager.js"/>
+<script type="application/javascript" src="BrowserView.js"/>
+<script type="application/javascript">
+<![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 = scrollbox.scrolledWidth;
+ let h = scrollbox.scrolledHeight;
+ let container = document.getElementById("tile_container");
+ let [x, y] = getScrollboxPosition();
+ 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 (let ijstr of ijstrs) {
+ let [i, j] = ijstr.split(',').map(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 = scrollbox.positionX;
+ let y = scrollbox.positionY;
+ let w = scrollbox.scrolledWidth;
+ let h = scrollbox.scrolledHeight;
+
+ 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 + dx, 0);
+ let newY = Math.max(y + dy, 0);
+
+ if (newX < w || newY < h) {
+ // clip dx and dy to prevent us from going below 0
+ dx = Math.max(dx, -x);
+ dy = Math.max(dy, -y);
+
+ let oldx = x;
+ let oldy = y;
+
+ 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;
+ 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() {
+ return [scrollbox.positionX, scrollbox.positionY];
+}
+
+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" remote="true" style="width: 1024px; height: 614px"/> -->
+ <iframe id="googlenews" src="http://news.google.com/" type="content" remote="true" style="width: 1024px; height: 614px"/>
+ </html:div>
+ </html:div>
+ </box>
+
+</window>
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/main.xul b/toolkit/content/tests/fennec-tile-testapp/chrome/content/main.xul
new file mode 100644
index 000000000..f829b3f4a
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/main.xul
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+
+<window id="main" title="My App" width="300" height="300"
+xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <caption label="Hello World"/>
+</window>
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/content/overlay.js b/toolkit/content/tests/fennec-tile-testapp/chrome/content/overlay.js
new file mode 100644
index 000000000..8dd09af00
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/content/overlay.js
@@ -0,0 +1,15 @@
+var tile = {
+ onLoad: function() {
+ // initialization code
+ this.initialized = true;
+ this.strings = document.getElementById("tile-strings");
+ },
+ onMenuItemCommand: function(e) {
+ var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ promptService.alert(window, this.strings.getString("helloMessageTitle"),
+ this.strings.getString("helloMessage"));
+ },
+
+};
+window.addEventListener("load", function(e) { tile.onLoad(e); }, false);
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.dtd b/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.dtd
new file mode 100644
index 000000000..8cffbce35
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.dtd
@@ -0,0 +1 @@
+<!ENTITY tile.label "Your localized menuitem">
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.properties b/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.properties
new file mode 100644
index 000000000..72062a4f0
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/locale/en-US/tile.properties
@@ -0,0 +1,3 @@
+helloMessage=Hello World!
+helloMessageTitle=Hello
+prefMessage=Int Pref Value: %d
diff --git a/toolkit/content/tests/fennec-tile-testapp/chrome/skin/overlay.css b/toolkit/content/tests/fennec-tile-testapp/chrome/skin/overlay.css
new file mode 100644
index 000000000..98718057f
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/chrome/skin/overlay.css
@@ -0,0 +1,5 @@
+/* This is just an example. You shouldn't do this. */
+#tile-hello
+{
+ color: red ! important;
+}
diff --git a/toolkit/content/tests/fennec-tile-testapp/defaults/preferences/prefs.js b/toolkit/content/tests/fennec-tile-testapp/defaults/preferences/prefs.js
new file mode 100644
index 000000000..823196f8b
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/defaults/preferences/prefs.js
@@ -0,0 +1,2 @@
+pref("toolkit.defaultChromeURI", "chrome://tile/content/foo.xul");
+pref("browser.dom.window.dump.enabled", true);
diff --git a/toolkit/content/tests/fennec-tile-testapp/install.rdf b/toolkit/content/tests/fennec-tile-testapp/install.rdf
new file mode 100644
index 000000000..e80fb845c
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/install.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>tile@roy</em:id>
+ <em:name>tile</em:name>
+ <em:version>1.0</em:version>
+ <em:creator>Roy</em:creator>
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!-- firefox -->
+ <em:minVersion>1.5</em:minVersion>
+ <em:maxVersion>3.5.*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/content/tests/fennec-tile-testapp/logread.py b/toolkit/content/tests/fennec-tile-testapp/logread.py
new file mode 100644
index 000000000..afa1fa524
--- /dev/null
+++ b/toolkit/content/tests/fennec-tile-testapp/logread.py
@@ -0,0 +1,104 @@
+#!/usr/bin/python
+import re, sys
+
+interesting_re = re.compile("(js_Execute|CallHook) ([^ ]+) ([^ ]+ )?([^ ]+ms)")
+class Entry:
+ def __init__(self, kind, depth, file, linenum, func, timetaken):
+ self.kind = kind
+ self.depth = depth
+ self.file = file
+ self.linenum = linenum
+ self.func = func
+ self.timetaken = timetaken
+ self.calls = 0
+ self.duration = 0
+
+ def __str__(self):
+ return " ".join(map(str,[self.kind, self.depth, self.file, self.linenum, self.func, self.timetaken]))
+
+ def id(self):
+ if self.kind == "js_Execute":
+ return self.file
+ else:
+ if self.file and self.linenum:
+ strout = "%s:%d" % (self.file, self.linenum)
+ if self.func:
+ strout = "%s %s" % (self.func, strout)
+ return strout
+ elif self.func:
+ return self.func
+ else:
+ print("No clue what my id is:"+self)
+
+ def call(self, timetaken):
+ self.calls += 1
+ self.duration += timetaken
+
+def parse_line(line):
+ m = interesting_re.search(line)
+ if not m:
+ return None
+
+ ms_index = line.find("ms")
+ depth = m.start() - ms_index - 3
+ kind = m.group(1)
+ func = None
+ file = None
+ linenum = None
+ if kind == "CallHook":
+ func = m.group(2)
+ file = m.group(3)
+ colpos = file.rfind(":")
+ (file,linenum) = file[:colpos], file[colpos+1:-1]
+ if linenum == "0":
+ linenum = None
+ else:
+ linenum = int(linenum)
+ offset = 1
+ else:
+ file = m.group(3)
+
+ timetaken = None
+ try:
+ timetaken = float(m.group(4)[:-2])
+ except:
+ return None
+ return Entry(kind, depth, file, linenum, func, timetaken)
+
+def compare(x,y):
+ diff = x[1].calls - y[1].calls
+ if diff == 0:
+ return int(x[1].duration - y[1].duration)
+ elif diff > 0:
+ return 1
+ elif diff < 0:
+ return -1
+
+def frequency(ls):
+ dict = {}
+ for item in ls:
+ id = item.id()
+ stat = None
+ if not id in dict:
+ stat = dict[id] = item
+ else:
+ stat = dict[id]
+ stat.call(item.timetaken)
+
+ ls = dict.items()
+ ls.sort(compare)
+ ls = filter(lambda (_,item): item.duration > 20, ls)
+# ls = filter(lambda (_,item): item.file and item.file.find("browser.js") != -1 and item.linenum <= 1223 and item.linenum >1067, ls)
+ for key, item in ls:
+ print(item.calls,key, str(item.duration)+"ms")
+
+def go():
+ file = sys.argv[1]
+
+ ls = filter(lambda x: x != None, map(parse_line, open(file).readlines()))
+
+ frequency(ls)
+ print ls[0]
+
+go()
+