diff options
Diffstat (limited to 'toolkit/content/tests/fennec-tile-testapp')
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 0000000000..510d481809 --- /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 0000000000..118354c81a --- /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 0000000000..c498810df3 --- /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 0000000000..49cbbed664 --- /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 0000000000..52beb6e364 --- /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 0000000000..69288e725f --- /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 0000000000..612f8bb9ff --- /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 0000000000..cdc01658a0 --- /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 0000000000..f829b3f4a1 --- /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 0000000000..8dd09af00d --- /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 0000000000..8cffbce359 --- /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 0000000000..72062a4f0b --- /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 0000000000..98718057f4 --- /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 0000000000..823196f8bf --- /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 0000000000..e80fb845cc --- /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 0000000000..afa1fa524b --- /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() + |