gecko/mobile/chrome/content/TileManager.js.in

955 lines
32 KiB
JavaScript

// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/*
* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Mozilla Mobile Browser.
*
* The Initial Developer of the Original Code is
* Mozilla Corporation.
* Portions created by the Initial Developer are Copyright (C) 2009
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Roy Frostig <rfrostig@mozilla.com>
* Stuart Parmenter <stuart@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
#define BEGIN_FOREACH_IN_RECT(rect, tilecache, tile) \
{ \
let __starti = (rect).left >> kTileExponentWidth; \
let __endi = (rect).right >> kTileExponentWidth; \
let __startj = (rect).top >> kTileExponentHeight; \
let __endj = (rect).bottom >> kTileExponentHeight; \
\
let tile = null; \
let __i, __j; \
for (__j = __startj; __j <= __endj; ++__j) { \
for (__i = __starti; __i <= __endi; ++__i) { \
tile = (tilecache).getTile(__i, __j, false, null); \
if (tile) {
#define END_FOREACH_IN_RECT \
} \
} \
} \
}
#define BEGIN_FORCREATE_IN_RECT(rect, tilecache, tile) \
{ \
let __starti = (rect).left >> kTileExponentWidth; \
let __endi = (rect).right >> kTileExponentWidth; \
let __startj = (rect).top >> kTileExponentHeight; \
let __endj = (rect).bottom >> kTileExponentHeight; \
\
let tile = null; \
let __i, __j; \
for (__j = __startj; __j <= __endj; ++__j) { \
for (__i = __starti; __i <= __endi; ++__i) { \
tile = (tilecache).getTile(__i, __j, true, null); \
if (tile) { \
#define END_FORCREATE_IN_RECT \
} \
} \
} \
}
#define FOREACH_IN_RECT(rect, tilecache, fn, thisObj) \
BEGIN_FOREACH_IN_RECT(rect, tilecache, tile) \
(fn).call((thisObj), tile); \
END_FOREACH_IN_RECT
#define FORCREATE_IN_RECT(rect, tilecache, fn, thisObj) \
BEGIN_FORCREATE_IN_RECT(rect, tilecache, tile) \
(fn).call((thisObj), tile); \
END_FORCREATE_IN_RECT
const kXHTMLNamespaceURI = "http://www.w3.org/1999/xhtml";
// base-2 exponent for width, height of a single tile.
const kTileExponentWidth = 9;
const kTileExponentHeight = 9;
const kTileWidth = Math.pow(2, kTileExponentWidth); // 2^9 = 512
const kTileHeight = Math.pow(2, kTileExponentHeight); // 2^9 = 512
const kTileCrawlTimeCap = 100; // millis
const kTileCrawlComeAgain = 0; // millis
// Helper used to hide IPC / non-IPC differences for renering to a canvas
function rendererFactory(isRemote, aCanvas, aBrowser) {
if (isRemote) {
let wrapper = {};
wrapper.ctx = aCanvas.MozGetIPCContext("2d");
wrapper.drawContent = function(aLeft, aTop, aWidth, aHeight, aColor, aFlags) {
this.ctx.asyncDrawXULElement(aBrowser, aLeft, aTop, aWidth, aHeight, aColor, aFlags);
};
return wrapper;
}
let wrapper = {};
wrapper.ctx = aCanvas.getContext("2d");
wrapper.drawContent = function(aLeft, aTop, aWidth, aHeight, aColor, aFlags) {
this.ctx.drawWindow(aBrowser.contentWindow, aLeft, aTop, aWidth, aHeight, aColor, aFlags);
};
return wrapper;
}
/**
* 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.
*/
function TileManager(appendTile, removeTile, browserView, cacheSize) {
/* 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, cacheSize);
/* 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, and tiles
* within this rectangle should not be evicted for use elsewhere. */
this._criticalRect = new Rect(0, 0, 0, 0);
/* timeout of the non-visible-tiles-crawler to cache renders from the browser */
this._idleTileCrawlerTimeout = 0;
/* object that keeps state on our current prefetch crawl */
this._crawler = new TileManager.CrawlIterator(this._tileCache, new Rect(0, 0, 0, 0));
/* remember if critical rect was changed so that we only do hard work one time */
this._lastCriticalRect = new Rect(0, 0, 0, 0);
/* if true, fetch offscreen dirty tiles in the "background" */
this._prefetch = false;
/* create one Image of the checkerboard to be reused */
this._checkerboard = new Image();
this._checkerboard.src = "chrome://browser/content/checkerboard.png";
}
TileManager.prototype = {
/**
* Entry point by which the BrowserView informs of changes to the viewport or
* critical rect.
*/
viewportChangeHandler: function viewportChangeHandler(viewportRect,
criticalRect,
boundsSizeChanged,
dirtyAll) {
let tc = this._tileCache;
let iBoundOld = tc.iBound;
let jBoundOld = tc.jBound;
let iBound = tc.iBound = Math.ceil(viewportRect.right / kTileWidth) - 1;
let jBound = tc.jBound = Math.ceil(viewportRect.bottom / kTileHeight) - 1;
if (criticalRect.isEmpty() || !criticalRect.equals(this._criticalRect)) {
this.criticalMove(criticalRect, !(dirtyAll || boundsSizeChanged));
} else {
// The critical rect hasn't changed, but there are possibly more tiles lounging about offscreen,
// waiting to be rendered. Make sure crawler and eviction knows about them.
this.recenterCrawler();
}
if (dirtyAll) {
this.dirtyRects([viewportRect.clone()], true);
} else if (boundsSizeChanged) {
// This is a special case. The bounds size changed, but we are
// told that not everything is dirty (so mayhap content grew or
// shrank vertically or horizontally). We might have old tiles
// around in those areas just due to the fact that they haven't
// been yet evicted, so we patrol the new regions in search of
// any such leftover tiles and mark those we find as dirty.
//
// The two dirty rects below mark dirty any renegade tiles in
// the newly annexed grid regions as per the following diagram
// of the "new" viewport.
//
// +------------+------+
// |old | A |
// |viewport | |
// | | |
// | | |
// | | |
// +------------+ |
// | B | |
// | | |
// +------------+------+
//
// The first rectangle covers annexed region A, the second
// rectangle covers annexed region B.
//
// XXXrf If the tiles are large, then we are creating some
// redundant work here by invalidating the entire tile that
// the old viewport boundary crossed (note markDirty() being
// called with no rectangle parameter). The rectangular area
// within the tile lying beyond the old boundary is certainly
// dirty, but not the area before. Moreover, since we mark
// dirty entire tiles that may cross into the old viewport,
// they might, in particular, cross into the critical rect
// (which is anyhwere in the old viewport), so we call a
// criticalRectPaint() for such cleanup. We do all this more
// or less because we don't have much of a notion of "the old
// viewport" here except for in the sense that we know the
// index bounds on the tilecache grid from before (and the new
// index bounds now).
//
let t, l, b, r, rect;
let rects = [];
if (iBoundOld <= iBound) {
l = iBoundOld * kTileWidth;
t = 0;
r = (iBound + 1) * kTileWidth;
b = (jBound + 1) * kTileHeight;
rect = new Rect(l, t, r - l, b - t);
rect.restrictTo(viewportRect);
if (!rect.isEmpty())
rects.push(rect);
}
if (jBoundOld <= jBound) {
l = 0;
t = jBoundOld * kTileHeight;
r = (iBound + 1) * kTileWidth;
b = (jBound + 1) * kTileHeight;
rect = new Rect(l, t, r - l, b - t);
rect.restrictTo(viewportRect);
if (!rect.isEmpty())
rects.push(rect);
}
this.dirtyRects(rects, true);
}
},
/**
* Erase everything in these rects. Useful for tiles that are outside of the viewport rect but
* still visible.
*/
clearRects: function clearRects(rects) {
let criticalIsDirty = false;
let criticalRect = this._criticalRect;
let tc = this._tileCache;
let rect;
for (let i = rects.length - 1; i >= 0; --i) {
rect = rects[i];
BEGIN_FOREACH_IN_RECT(rect, tc, tile)
tile.clear(rect);
END_FOREACH_IN_RECT
}
if (criticalIsDirty && doCriticalRender)
this.criticalRectPaint();
},
dirtyRects: function dirtyRects(rects, doCriticalRender, keepTileInDom) {
let outsideIsDirty = false;
let criticalIsDirty = false;
let criticalRect = this._criticalRect;
let tc = this._tileCache;
let crawler = this._crawler;
for (let i = 0, len = rects.length; i < len; ++i) {
let rect = rects[i];
BEGIN_FOREACH_IN_RECT(rect, tc, tile)
if (!tile.boundRect.intersects(criticalRect)) {
// Dirty tile outside of viewport. Just remove and redraw later.
if (!keepTileInDom)
this._removeTileSafe(tile);
crawler.enqueue(tile.i, tile.j);
outsideIsDirty = true;
} else {
criticalIsDirty = true;
}
tile.markDirty(rects[i]);
END_FOREACH_IN_RECT
}
if (criticalIsDirty && doCriticalRender)
this.criticalRectPaint();
if (outsideIsDirty)
this.restartPrefetchCrawl();
},
criticalRectPaint: function criticalRectPaint() {
let cr = this._criticalRect;
let lastCr = this._lastCriticalRect;
if (!lastCr.isEmpty()) {
// This is the first paint since the last critical move.
let tc = this._tileCache;
BEGIN_FOREACH_IN_RECT(lastCr, tc, tile)
tc.releaseTile(tile);
END_FOREACH_IN_RECT
lastCr.setRect(0, 0, 0, 0);
if (!cr.isEmpty())
this.recenterEvictionQueue(cr.center().map(Math.round));
this.recenterCrawler();
}
if (!cr.isEmpty())
this._renderAppendHoldRect(cr);
},
criticalMove: function criticalMove(destCriticalRect, doCriticalPaint) {
let cr = this._criticalRect;
let lastCr = this._lastCriticalRect;
if (lastCr.isEmpty() && !cr.equals(destCriticalRect))
lastCr.copyFrom(cr);
cr.copyFrom(destCriticalRect);
if (doCriticalPaint)
this.criticalRectPaint();
},
setPrefetch: function setPrefetch(prefetch) {
if (prefetch != this._prefetch) {
this._prefetch = prefetch;
if (prefetch)
this.restartPrefetchCrawl();
else
this.stopPrefetchCrawl();
}
},
restartPrefetchCrawl: function restartPrefetchCrawl() {
if (this._prefetch && !this._idleTileCrawlerTimeout)
this._idleTileCrawlerTimeout = setTimeout(this._idleTileCrawler, kTileCrawlComeAgain, this);
},
stopPrefetchCrawl: function stopPrefetchCrawl() {
if (this._idleTileCrawlerTimeout)
clearTimeout(this._idleTileCrawlerTimeout);
delete this._idleTileCrawlerTimeout;
},
recenterEvictionQueue: function recenterEvictionQueue(ctr) {
let ctri = ctr.x >> kTileExponentWidth;
let ctrj = ctr.y >> kTileExponentHeight;
this._tileCache.sortEvictionQueue(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;
});
},
/** Crawler will recalculate the tiles it is supposed to fetch in the background. */
recenterCrawler: function recenterCrawler() {
let cr = this._criticalRect;
this._crawler.recenter(cr.clone());
this.restartPrefetchCrawl();
},
/**
* Render a rect to the canvas under the given scale.
*/
renderRectToCanvas: function renderRectToCanvas(srcRect, destCanvas, scalex, scaley, drawMissing) {
let bv = this._browserView;
let tc = this._tileCache;
let renderer = rendererFactory(!bv._contentWindow, destCanvas, bv._browser);
let ctx = renderer.ctx;
bv.viewportToBrowserRect(srcRect);
ctx.save();
bv.browserToViewportCanvasContext(ctx);
ctx.scale(scalex, scaley);
renderer.drawContent(srcRect.left, srcRect.top, srcRect.width, srcRect.height,
"white",
(ctx.DRAWWINDOW_DO_NOT_FLUSH | ctx.DRAWWINDOW_DRAW_CARET));
ctx.restore();
},
_showTile: function _showTile(tile) {
if (tile.isDirty())
tile.render(this._browserView);
this._appendTileSafe(tile);
},
_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;
}
},
_renderAppendHoldRect: function _renderAppendHoldRect(rect) {
let tc = this._tileCache;
// XXX this can evict crawl tiles that might just be rerendered later. It would be nice if
// getTile understood which tiles had priority so that we don't waste time.
BEGIN_FORCREATE_IN_RECT(rect, tc, tile)
this._showTile(tile);
this._tileCache.holdTile(tile);
END_FORCREATE_IN_RECT
},
_idleTileCrawler: function _idleTileCrawler(self) {
if (!self)
self = this;
let start = Date.now();
let comeAgain = true;
let tile;
while ((Date.now() - start) <= kTileCrawlTimeCap) {
tile = self._crawler.next();
if (!tile) {
comeAgain = false;
break;
}
self._showTile(tile);
}
if (comeAgain) {
self._idleTileCrawlerTimeout = setTimeout(self._idleTileCrawler, kTileCrawlComeAgain, self);
} else {
self.stopPrefetchCrawl();
}
}
};
/**
* 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._tileMap = [];
// holds the same tiles that _tileMap holds, but as contiguous array
// elements, for pooling tiles for reuse under finite capacity
this._tilePool = [];
this._pos = -1;
this._capacity = capacity;
this._onBeforeTileDetach = onBeforeTileDetach;
this.iBound = iBound;
this.jBound = jBound;
};
TileManager.TileCache.prototype = {
get size() { return this._tilePool.length; },
/**
* The default tile comparison function used to order the tile pool such that
* tiles most eligible for eviction have higher index.
*
* Unless reset, this is a comparison function that will compare free tiles
* as greater than all non-free tiles. Useful, for instance, to shrink
* the tile pool when capacity is lowered, as we want to remove all tiles
* at the new cap and beyond, favoring removal of free tiles first.
*/
evictionCmp: function evictionCmp(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;
},
lookup: function lookup(i, j) {
let tile = null;
if (this._tileMap[i])
tile = this._tileMap[i][j] || null;
return tile;
},
getCapacity: function getCapacity() { return this._capacity; },
inBounds: function inBounds(i, j) {
return (0 <= i && 0 <= j && i <= this.iBound && j <= this.jBound);
},
sortEvictionQueue: function sortEvictionQueue(cmp) {
let pool = this._tilePool;
pool.sort(cmp ? cmp : this.evictionCmp);
this._pos = pool.length - 1;
},
/**
* 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 = this.lookup(i, j);
if (!tile && 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).
*/
holdTile: function holdTile(tile) {
if (tile) tile.free = false;
},
/**
* Release a tile (i.e. mark it free).
*/
releaseTile: function releaseTile(tile) {
if (tile) tile.free = true;
},
_detachTile: function _detachTile(i, j) {
let tile = this.lookup(i, j);
if (tile) {
if (this._onBeforeTileDetach)
this._onBeforeTileDetach(tile);
this.releaseTile(tile);
delete this._tileMap[i][j];
}
return tile;
},
/**
* Pluck tile from its current tileMap position and drop it in position
* given by (i, j).
*/
_reassignTile: function _reassignTile(tile, i, j) {
this._detachTile(tile.i, tile.j); // detach
tile.init(i, j); // re-init
this._tileMap[i][j] = tile; // attach
return tile;
},
_evictTile: function _evictTile(evictionGuard) {
let k = this._pos;
let pool = this._tilePool;
let victim = null;
let tile;
for (; k >= 0; --k) {
tile = pool[k];
if (!this.inBounds(tile.i, tile.j) || tile.free && ( !evictionGuard || evictionGuard(tile) )) {
victim = tile;
--k;
break;
}
}
this._pos = k;
return victim;
},
_createTile: function _createTile(i, j, evictionGuard) {
if (!this._tileMap[i])
this._tileMap[i] = [];
let tile = null;
if (this._tilePool.length < this._capacity) { // either capacity is
tile = new TileManager.Tile(i, j); // infinite, or we have
this._tileMap[i][j] = tile; // room to allocate more
this._tilePool.push(tile);
} else {
tile = this._evictTile(evictionGuard);
if (tile)
this._reassignTile(tile, i, j);
}
return tile;
}
};
/**
* A tile is a little object with an <html:canvas> DOM element that it used for
* caching renders from drawWindow().
*
* Supports the dirtying of only a subrectangle of the bounding rectangle, as
* well as just dirtying the entire tile.
*/
TileManager.Tile = function Tile(i, j) {
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.init(i, j); // defines more properties, cf below
};
TileManager.Tile.prototype = {
init: function init(i, j) {
if (!this.boundRect)
this.boundRect = new Rect(i * kTileWidth, j * kTileHeight, kTileWidth, kTileHeight);
else
this.boundRect.setRect(i * kTileWidth, j * kTileHeight, kTileWidth, kTileHeight);
/* indices of this tile in the tile cache's tile grid map */
this.i = i; // row
this.j = j; // column
/* flag used by TileManager to avoid re-appending tiles that have already
* been appended */
this.appended = false;
/* flag used by the TileCache to mark tiles as ineligible for eviction,
* usually because they are fixed in some critical position */
this.free = true;
/* 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 = new Rect(0, 0, 0, 0);
},
get x() { return this.boundRect.left; },
get y() { return this.boundRect.top; },
/**
* Get the <html:canvas> DOM element with this tile's cached paint.
*/
getContentImage: function getContentImage() { return this._canvas; },
isDirty: function isDirty() { return this._dirtyTileCanvas; },
/** Clear region in rect. */
clear: function clear(rect) {
let boundRect = this.boundRect;
let region = rect.intersect(boundRect).expandToIntegers().translate(-boundRect.left, -boundRect.top);
let ctx = this._canvas.getContext("2d");
ctx.fillStyle = "white";
ctx.fillRect(region.left, region.top, region.right - region.left, region.bottom - region.top);
},
/**
* 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 (i.e. the whole tile needs to be rendered
* on next render).
*/
markDirty: function markDirty(dirtyRect) {
if (!dirtyRect) {
this._dirtyTileCanvasRect.copyFrom(this.boundRect);
} else {
this._dirtyTileCanvasRect.expandToContain(dirtyRect.intersect(this.boundRect)).expandToIntegers();
}
// XXX if, after the above, the dirty rectangle takes up a large enough
// portion of the boundRect, we should probably just mark the entire tile
// dirty and fastpath for future calls.
if (!this._dirtyTileCanvasRect.isEmpty())
this._dirtyTileCanvas = true;
},
unmarkDirty: function unmarkDirty() {
this._dirtyTileCanvasRect.setRect(0, 0, 0, 0);
this._dirtyTileCanvas = false;
},
/**
* Actually draw the browser content into the dirty region of this tile. This
* requires us to actually draw with nsIDOMCanvasRenderingContext2D::
* drawWindow(), which we expect to be a 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(browserView) {
if (!this.isDirty())
this.markDirty();
let rect = this._dirtyTileCanvasRect;
let x = rect.left - this.boundRect.left;
let y = rect.top - this.boundRect.top;
browserView.viewportToBrowserRect(rect);
let renderer = rendererFactory(!browserView._contentWindow, this._canvas, browserView._browser);
let ctx = renderer.ctx;
ctx.save();
ctx.translate(x, y);
browserView.browserToViewportCanvasContext(ctx);
// We expand the rect to working around a gfx issue (bug 534054)
renderer.drawContent(rect.left , rect.top, rect.right - rect.left + 1, rect.bottom - rect.top + 1,
"white",
(ctx.DRAWWINDOW_DRAW_CARET));
ctx.restore();
this.unmarkDirty();
},
/**
* Standard toString prints "Tile(<row>, <column>)", but if `more' flags true
* then some extra information is printed.
*/
toString: function toString(more) {
if (more) {
return 'Tile(' + this.i + ', '
+ this.j + ', '
+ 'dirty=' + this.isDirty() + ', '
+ 'boundRect=' + this.boundRect.toString() + ')';
}
return 'Tile(' + this.i + ', ' + this.j + ')';
}
};
/**
* A CrawlIterator is in charge of creating and returning subsequent tiles "crawled"
* over as we render tiles lazily.
*
* 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 tc The TileCache over whose tiles this CrawlIterator will crawl
* @param rect The rectangle that we grow in the first (rectangle * expansion)
* iteration state. If empty, doesn't crawl.
*/
TileManager.CrawlIterator = function CrawlIterator(tc, rect) {
this._tileCache = tc;
this.recenter(rect);
};
TileManager.CrawlIterator.prototype = {
_generateCrawlQueue: function _generateCrawlQueue(rect) {
function add(i, j) {
if (tc.inBounds(i, j)) {
outOfBounds = false;
result.push([i, j]);
--index;
return true;
}
return false;
}
let tc = this._tileCache;
let capacity = tc.getCapacity();
let result = [];
let index = capacity;
let outOfBounds;
let counter;
let starti = rect.left >> kTileExponentWidth;
let endi = rect.right >> kTileExponentWidth;
let startj = rect.top >> kTileExponentHeight;
let endj = rect.bottom >> kTileExponentHeight;
let i, j;
while (!outOfBounds) {
starti -= 1;
endi += 1;
startj -= 1;
endj += 1;
outOfBounds = true;
// top, bottom rect borders
for each (j in [startj, endj]) {
for (counter = 1, i = Math.floor((starti + endi) / 2); i >= starti && i <= endi;) {
if (add(i, j) && index == 0)
return result;
i += counter;
counter = -counter + (counter > 0 ? -1 : 1);
}
}
// left, right rect borders
for each (i in [starti, endi]) {
counter = 1;
for (counter = 1, j = Math.floor((startj + endj) / 2); j >= startj && j <= endj;) {
if (add(i, j) && index == 0)
return result;
j += counter;
counter = -counter + (counter > 0 ? -1 : 1);
}
}
}
return result;
},
recenter: function recenter(rect) {
// Queue should not be very big, so put first priorities last in order to pop quickly.
this._crawlQueue = rect.isEmpty() ? [] : this._generateCrawlQueue(rect).reverse();
// 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.i + "," + tile.j]; };
// after we finish the rectangle iteration state, we enter the FILO queue state
// no need to remember old dirty tiles, if it's important we'll get to it anyways
this._queue = [];
// use a dictionary to prevent tiles from being enqueued twice --- "patience, we'll get to
// it in a moment"
this._enqueued = {};
},
next: function next() {
// Priority for next goes to the crawl queue, dirty tiles afterwards. Since dirty
// tile queue does not really have a necessary order, pop off the top.
let coords = this._crawlQueue.pop() || this.dequeue();
let tile = null;
if (coords) {
let [i, j] = coords;
// getTile will create a tile only if there are any left in our capacity that have not been
// visited already by the crawler.
tile = this._tileCache.getTile(i, j, true, this._notVisited);
if (tile) {
this._visited[this._strIndices(i, j)] = true;
} else {
tile = this.next();
}
}
return tile;
},
dequeue: function dequeue() {
if (this._queue.length) {
let [i, j] = this._queue.pop();
this._enqueued[this._strIndices(i, j)] = false;
return [i, j];
} else {
return null;
}
},
enqueue: function enqueue(i, j) {
let index = this._strIndices(i, j);
let enqueued = this._enqueued;
if (!enqueued[index]) {
this._queue.push([i, j]);
enqueued[index] = true;
}
},
_strIndices: function _strIndices(i, j) {
return i + "," + j;
},
};