/toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js

http://github.com/zpao/v8monkey · JavaScript · 728 lines · 372 code · 143 blank · 213 comment · 34 complexity · 833e7700b16f76157ea102083a43ca1a MD5 · raw file

  1. // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
  2. /*
  3. * ***** BEGIN LICENSE BLOCK *****
  4. * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  5. *
  6. * The contents of this file are subject to the Mozilla Public License Version
  7. * 1.1 (the "License"); you may not use this file except in compliance with
  8. * the License. You may obtain a copy of the License at
  9. * http://www.mozilla.org/MPL/
  10. *
  11. * Software distributed under the License is distributed on an "AS IS" basis,
  12. * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  13. * for the specific language governing rights and limitations under the
  14. * License.
  15. *
  16. * The Original Code is Mozilla Mobile Browser.
  17. *
  18. * The Initial Developer of the Original Code is
  19. * Mozilla Corporation.
  20. * Portions created by the Initial Developer are Copyright (C) 2008
  21. * the Initial Developer. All Rights Reserved.
  22. *
  23. * Contributor(s):
  24. * Roy Frostig <rfrostig@mozilla.com>
  25. *
  26. * Alternatively, the contents of this file may be used under the terms of
  27. * either the GNU General Public License Version 2 or later (the "GPL"), or
  28. * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  29. * in which case the provisions of the GPL or the LGPL are applicable instead
  30. * of those above. If you wish to allow use of your version of this file only
  31. * under the terms of either the GPL or the LGPL, and not to allow others to
  32. * use your version of this file under the terms of the MPL, indicate your
  33. * decision by deleting the provisions above and replace them with the notice
  34. * and other provisions required by the GPL or the LGPL. If you do not delete
  35. * the provisions above, a recipient may use your version of this file under
  36. * the terms of any one of the MPL, the GPL or the LGPL.
  37. *
  38. * ***** END LICENSE BLOCK ***** */
  39. let Ci = Components.interfaces;
  40. // --- REMOVE ---
  41. let noop = function() {};
  42. let endl = '\n';
  43. // --------------
  44. function BrowserView(container, visibleRect) {
  45. bindAll(this);
  46. this.init(container, visibleRect);
  47. }
  48. /**
  49. * A BrowserView maintains state of the viewport (browser, zoom level,
  50. * dimensions) and the visible rectangle into the viewport, for every
  51. * browser it is given (cf setBrowser()). In updates to the viewport state,
  52. * a BrowserView (using its TileManager) renders parts of the page quasi-
  53. * intelligently, with guarantees of having rendered and appended all of the
  54. * visible browser content (aka the "critical rectangle").
  55. *
  56. * State is characterized in large part by two rectangles (and an implicit third):
  57. * - Viewport: Always rooted at the origin, ie with (left, top) at (0, 0). The
  58. * width and height (right and bottom) of this rectangle are that of the
  59. * current viewport, which corresponds more or less to the transformed
  60. * browser content (scaled by zoom level).
  61. * - Visible: Corresponds to the client's viewing rectangle in viewport
  62. * coordinates. Has (top, left) corresponding to position, and width & height
  63. * corresponding to the clients viewing dimensions. Take note that the top
  64. * and left of the visible rect are per-browser state, but that the width
  65. * and height persist across setBrowser() calls. This is best explained by
  66. * a simple example: user views browser A, pans to position (x0, y0), switches
  67. * to browser B, where she finds herself at position (x1, y1), tilts her
  68. * device so that visible rectangle's width and height change, and switches
  69. * back to browser A. She expects to come back to position (x0, y0), but her
  70. * device remains tilted.
  71. * - Critical (the implicit one): The critical rectangle is the (possibly null)
  72. * intersection of the visible and viewport rectangles. That is, it is that
  73. * region of the viewport which is visible to the user. We care about this
  74. * because it tells us which region must be rendered as soon as it is dirtied.
  75. * The critical rectangle is mostly state that we do not keep in BrowserView
  76. * but that our TileManager maintains.
  77. *
  78. * Example rectangle state configurations:
  79. *
  80. *
  81. * +-------------------------------+
  82. * |A |
  83. * | |
  84. * | |
  85. * | |
  86. * | +----------------+ |
  87. * | |B,C | |
  88. * | | | |
  89. * | | | |
  90. * | | | |
  91. * | +----------------+ |
  92. * | |
  93. * | |
  94. * | |
  95. * | |
  96. * | |
  97. * +-------------------------------+
  98. *
  99. *
  100. * A = viewport ; at (0, 0)
  101. * B = visible ; at (x, y) where x > 0, y > 0
  102. * C = critical ; at (x, y)
  103. *
  104. *
  105. *
  106. * +-------------------------------+
  107. * |A |
  108. * | |
  109. * | |
  110. * | |
  111. * +----+-----------+ |
  112. * |B .C | |
  113. * | . | |
  114. * | . | |
  115. * | . | |
  116. * +----+-----------+ |
  117. * | |
  118. * | |
  119. * | |
  120. * | |
  121. * | |
  122. * +-------------------------------+
  123. *
  124. *
  125. * A = viewport ; at (0, 0)
  126. * B = visible ; at (x, y) where x < 0, y > 0
  127. * C = critical ; at (0, y)
  128. *
  129. *
  130. * Maintaining per-browser state is a little bit of a hack involving attaching
  131. * an object as the obfuscated dynamic JS property of the browser object, that
  132. * hopefully no one but us will touch. See getViewportStateFromBrowser() for
  133. * the property name.
  134. */
  135. BrowserView.prototype = (
  136. function() {
  137. // -----------------------------------------------------------
  138. // Privates
  139. //
  140. const kZoomLevelMin = 0.2;
  141. const kZoomLevelMax = 4.0;
  142. const kZoomLevelPrecision = 10000;
  143. function visibleRectToCriticalRect(visibleRect, browserViewportState) {
  144. return visibleRect.intersect(browserViewportState.viewportRect);
  145. }
  146. function clampZoomLevel(zl) {
  147. let bounded = Math.min(Math.max(kZoomLevelMin, zl), kZoomLevelMax);
  148. return Math.round(bounded * kZoomLevelPrecision) / kZoomLevelPrecision;
  149. }
  150. function pageZoomLevel(visibleRect, browserW, browserH) {
  151. return clampZoomLevel(visibleRect.width / browserW);
  152. }
  153. function seenBrowser(browser) {
  154. return !!(browser.__BrowserView__vps);
  155. }
  156. function initBrowserState(browser, visibleRect) {
  157. let [browserW, browserH] = getBrowserDimensions(browser);
  158. let zoomLevel = pageZoomLevel(visibleRect, browserW, browserH);
  159. let viewportRect = (new wsRect(0, 0, browserW, browserH)).scale(zoomLevel, zoomLevel);
  160. dump('--- initing browser to ---' + endl);
  161. browser.__BrowserView__vps = new BrowserView.BrowserViewportState(viewportRect,
  162. visibleRect.x,
  163. visibleRect.y,
  164. zoomLevel);
  165. dump(browser.__BrowserView__vps.toString() + endl);
  166. dump('--------------------------' + endl);
  167. }
  168. function getViewportStateFromBrowser(browser) {
  169. return browser.__BrowserView__vps;
  170. }
  171. function getBrowserDimensions(browser) {
  172. return [browser.scrollWidth, browser.scrollHeight];
  173. }
  174. function getContentScrollValues(browser) {
  175. let cwu = getBrowserDOMWindowUtils(browser);
  176. let scrollX = {};
  177. let scrollY = {};
  178. cwu.getScrollXY(false, scrollX, scrollY);
  179. return [scrollX.value, scrollY.value];
  180. }
  181. function getBrowserDOMWindowUtils(browser) {
  182. return browser.contentWindow
  183. .QueryInterface(Ci.nsIInterfaceRequestor)
  184. .getInterface(Ci.nsIDOMWindowUtils);
  185. }
  186. function getNewBatchOperationState() {
  187. return {
  188. viewportSizeChanged: false,
  189. dirtyAll: false
  190. };
  191. }
  192. function clampViewportWH(width, height, visibleRect) {
  193. let minW = visibleRect.width;
  194. let minH = visibleRect.height;
  195. return [Math.max(width, minW), Math.max(height, minH)];
  196. }
  197. function initContainer(container, visibleRect) {
  198. container.style.width = visibleRect.width + 'px';
  199. container.style.height = visibleRect.height + 'px';
  200. container.style.overflow = '-moz-hidden-unscrollable';
  201. }
  202. function resizeContainerToViewport(container, viewportRect) {
  203. container.style.width = viewportRect.width + 'px';
  204. container.style.height = viewportRect.height + 'px';
  205. }
  206. // !!! --- RESIZE HACK BEGIN -----
  207. function simulateMozAfterSizeChange(browser, width, height) {
  208. let ev = document.createElement("MouseEvents");
  209. ev.initEvent("FakeMozAfterSizeChange", false, false, window, 0, width, height);
  210. browser.dispatchEvent(ev);
  211. }
  212. // !!! --- RESIZE HACK END -------
  213. // --- Change of coordinates functions --- //
  214. // The following returned object becomes BrowserView.prototype
  215. return {
  216. // -----------------------------------------------------------
  217. // Public instance methods
  218. //
  219. init: function init(container, visibleRect) {
  220. this._batchOps = [];
  221. this._container = container;
  222. this._browserViewportState = null;
  223. this._renderMode = 0;
  224. this._tileManager = new TileManager(this._appendTile, this._removeTile, this);
  225. this.setVisibleRect(visibleRect);
  226. // !!! --- RESIZE HACK BEGIN -----
  227. // remove this eventually
  228. this._resizeHack = {
  229. maxSeenW: 0,
  230. maxSeenH: 0
  231. };
  232. // !!! --- RESIZE HACK END -------
  233. },
  234. setVisibleRect: function setVisibleRect(r) {
  235. let bvs = this._browserViewportState;
  236. let vr = this._visibleRect;
  237. if (!vr)
  238. this._visibleRect = vr = r.clone();
  239. else
  240. vr.copyFrom(r);
  241. if (bvs) {
  242. bvs.visibleX = vr.left;
  243. bvs.visibleY = vr.top;
  244. // reclamp minimally to the new visible rect
  245. //this.setViewportDimensions(bvs.viewportRect.right, bvs.viewportRect.bottom);
  246. } else
  247. this._viewportChanged(false, false);
  248. },
  249. getVisibleRect: function getVisibleRect() {
  250. return this._visibleRect.clone();
  251. },
  252. getVisibleRectX: function getVisibleRectX() { return this._visibleRect.x; },
  253. getVisibleRectY: function getVisibleRectY() { return this._visibleRect.y; },
  254. getVisibleRectWidth: function getVisibleRectWidth() { return this._visibleRect.width; },
  255. getVisibleRectHeight: function getVisibleRectHeight() { return this._visibleRect.height; },
  256. setViewportDimensions: function setViewportDimensions(width, height, causedByZoom) {
  257. let bvs = this._browserViewportState;
  258. let vis = this._visibleRect;
  259. if (!bvs)
  260. return;
  261. //[width, height] = clampViewportWH(width, height, vis);
  262. bvs.viewportRect.right = width;
  263. bvs.viewportRect.bottom = height;
  264. // XXX we might not want the user's page to disappear from under them
  265. // at this point, which could happen if the container gets resized such
  266. // that visible rect becomes entirely outside of viewport rect. might
  267. // be wise to define what UX should be in this case, like a move occurs.
  268. // then again, we could also argue this is the responsibility of the
  269. // caller who would do such a thing...
  270. this._viewportChanged(true, !!causedByZoom);
  271. },
  272. setZoomLevel: function setZoomLevel(zl) {
  273. let bvs = this._browserViewportState;
  274. if (!bvs)
  275. return;
  276. let newZL = clampZoomLevel(zl);
  277. if (newZL != bvs.zoomLevel) {
  278. let browserW = this.viewportToBrowser(bvs.viewportRect.right);
  279. let browserH = this.viewportToBrowser(bvs.viewportRect.bottom);
  280. bvs.zoomLevel = newZL; // side-effect: now scale factor in transformations is newZL
  281. this.setViewportDimensions(this.browserToViewport(browserW),
  282. this.browserToViewport(browserH));
  283. }
  284. },
  285. getZoomLevel: function getZoomLevel() {
  286. let bvs = this._browserViewportState;
  287. if (!bvs)
  288. return undefined;
  289. return bvs.zoomLevel;
  290. },
  291. beginBatchOperation: function beginBatchOperation() {
  292. this._batchOps.push(getNewBatchOperationState());
  293. this.pauseRendering();
  294. },
  295. commitBatchOperation: function commitBatchOperation() {
  296. let bops = this._batchOps;
  297. if (bops.length == 0)
  298. return;
  299. let opState = bops.pop();
  300. this._viewportChanged(opState.viewportSizeChanged, opState.dirtyAll);
  301. this.resumeRendering();
  302. },
  303. discardBatchOperation: function discardBatchOperation() {
  304. let bops = this._batchOps;
  305. bops.pop();
  306. this.resumeRendering();
  307. },
  308. discardAllBatchOperations: function discardAllBatchOperations() {
  309. let bops = this._batchOps;
  310. while (bops.length > 0)
  311. this.discardBatchOperation();
  312. },
  313. moveVisibleBy: function moveVisibleBy(dx, dy) {
  314. let vr = this._visibleRect;
  315. let vs = this._browserViewportState;
  316. this.onBeforeVisibleMove(dx, dy);
  317. this.onAfterVisibleMove(dx, dy);
  318. },
  319. moveVisibleTo: function moveVisibleTo(x, y) {
  320. let visibleRect = this._visibleRect;
  321. let dx = x - visibleRect.x;
  322. let dy = y - visibleRect.y;
  323. this.moveBy(dx, dy);
  324. },
  325. /**
  326. * Calls to this function need to be one-to-one with calls to
  327. * resumeRendering()
  328. */
  329. pauseRendering: function pauseRendering() {
  330. this._renderMode++;
  331. },
  332. /**
  333. * Calls to this function need to be one-to-one with calls to
  334. * pauseRendering()
  335. */
  336. resumeRendering: function resumeRendering(renderNow) {
  337. if (this._renderMode > 0)
  338. this._renderMode--;
  339. if (renderNow || this._renderMode == 0)
  340. this._tileManager.criticalRectPaint();
  341. },
  342. isRendering: function isRendering() {
  343. return (this._renderMode == 0);
  344. },
  345. /**
  346. * @param dx Guess delta to destination x coordinate
  347. * @param dy Guess delta to destination y coordinate
  348. */
  349. onBeforeVisibleMove: function onBeforeVisibleMove(dx, dy) {
  350. let vs = this._browserViewportState;
  351. let vr = this._visibleRect;
  352. let destCR = visibleRectToCriticalRect(vr.clone().translate(dx, dy), vs);
  353. this._tileManager.beginCriticalMove(destCR);
  354. },
  355. /**
  356. * @param dx Actual delta to destination x coordinate
  357. * @param dy Actual delta to destination y coordinate
  358. */
  359. onAfterVisibleMove: function onAfterVisibleMove(dx, dy) {
  360. let vs = this._browserViewportState;
  361. let vr = this._visibleRect;
  362. vr.translate(dx, dy);
  363. vs.visibleX = vr.left;
  364. vs.visibleY = vr.top;
  365. let cr = visibleRectToCriticalRect(vr, vs);
  366. this._tileManager.endCriticalMove(cr, this.isRendering());
  367. },
  368. setBrowser: function setBrowser(browser, skipZoom) {
  369. let currentBrowser = this._browser;
  370. let browserChanged = (currentBrowser !== browser);
  371. if (currentBrowser) {
  372. currentBrowser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
  373. // !!! --- RESIZE HACK BEGIN -----
  374. // change to the real event type and perhaps refactor the handler function name
  375. currentBrowser.removeEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
  376. // !!! --- RESIZE HACK END -------
  377. this.discardAllBatchOperations();
  378. currentBrowser.setAttribute("type", "content");
  379. currentBrowser.docShell.isOffScreenBrowser = false;
  380. }
  381. this._restoreBrowser(browser);
  382. browser.setAttribute("type", "content-primary");
  383. this.beginBatchOperation();
  384. browser.addEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
  385. // !!! --- RESIZE HACK BEGIN -----
  386. // change to the real event type and perhaps refactor the handler function name
  387. browser.addEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
  388. // !!! --- RESIZE HACK END -------
  389. if (!skipZoom) {
  390. browser.docShell.isOffScreenBrowser = true;
  391. this.zoomToPage();
  392. }
  393. this._viewportChanged(browserChanged, browserChanged);
  394. this.commitBatchOperation();
  395. },
  396. handleMozAfterPaint: function handleMozAfterPaint(ev) {
  397. let browser = this._browser;
  398. let tm = this._tileManager;
  399. let vs = this._browserViewportState;
  400. let [scrollX, scrollY] = getContentScrollValues(browser);
  401. let clientRects = ev.clientRects;
  402. // !!! --- RESIZE HACK BEGIN -----
  403. // remove this, cf explanation in loop below
  404. let hack = this._resizeHack;
  405. let hackSizeChanged = false;
  406. // !!! --- RESIZE HACK END -------
  407. let rects = [];
  408. // loop backwards to avoid xpconnect penalty for .length
  409. for (let i = clientRects.length - 1; i >= 0; --i) {
  410. let e = clientRects.item(i);
  411. let r = new wsRect(e.left + scrollX,
  412. e.top + scrollY,
  413. e.width, e.height);
  414. this.browserToViewportRect(r);
  415. r.round();
  416. if (r.right < 0 || r.bottom < 0)
  417. continue;
  418. // !!! --- RESIZE HACK BEGIN -----
  419. // remove this. this is where we make 'lazy' calculations
  420. // that hint at a browser size change and fake the size change
  421. // event dispach
  422. if (r.right > hack.maxW) {
  423. hack.maxW = rect.right;
  424. hackSizeChanged = true;
  425. }
  426. if (r.bottom > hack.maxH) {
  427. hack.maxH = rect.bottom;
  428. hackSizeChanged = true;
  429. }
  430. // !!! --- RESIZE HACK END -------
  431. r.restrictTo(vs.viewportRect);
  432. rects.push(r);
  433. }
  434. // !!! --- RESIZE HACK BEGIN -----
  435. // remove this, cf explanation in loop above
  436. if (hackSizeChanged)
  437. simulateMozAfterSizeChange(browser, hack.maxW, hack.maxH);
  438. // !!! --- RESIZE HACK END -------
  439. tm.dirtyRects(rects, this.isRendering());
  440. },
  441. handleMozAfterSizeChange: function handleMozAfterPaint(ev) {
  442. // !!! --- RESIZE HACK BEGIN -----
  443. // get the correct properties off of the event, these are wrong because
  444. // we're using a MouseEvent since it has an X and Y prop of some sort and
  445. // we piggyback on that.
  446. let w = ev.screenX;
  447. let h = ev.screenY;
  448. // !!! --- RESIZE HACK END -------
  449. this.setViewportDimensions(w, h);
  450. },
  451. zoomToPage: function zoomToPage() {
  452. let browser = this._browser;
  453. if (!browser)
  454. return;
  455. let [w, h] = getBrowserDimensions(browser);
  456. this.setZoomLevel(pageZoomLevel(this._visibleRect, w, h));
  457. },
  458. zoom: function zoom(aDirection) {
  459. if (aDirection == 0)
  460. return;
  461. var zoomDelta = 0.05; // 1/20
  462. if (aDirection >= 0)
  463. zoomDelta *= -1;
  464. this.zoomLevel = this._zoomLevel + zoomDelta;
  465. },
  466. viewportToBrowser: function viewportToBrowser(x) {
  467. let bvs = this._browserViewportState;
  468. if (!bvs)
  469. throw "No browser is set";
  470. return x / bvs.zoomLevel;
  471. },
  472. browserToViewport: function browserToViewport(x) {
  473. let bvs = this._browserViewportState;
  474. if (!bvs)
  475. throw "No browser is set";
  476. return x * bvs.zoomLevel;
  477. },
  478. viewportToBrowserRect: function viewportToBrowserRect(rect) {
  479. let f = this.viewportToBrowser(1.0);
  480. return rect.scale(f, f);
  481. },
  482. browserToViewportRect: function browserToViewportRect(rect) {
  483. let f = this.browserToViewport(1.0);
  484. return rect.scale(f, f);
  485. },
  486. browserToViewportCanvasContext: function browserToViewportCanvasContext(ctx) {
  487. let f = this.browserToViewport(1.0);
  488. ctx.scale(f, f);
  489. },
  490. // -----------------------------------------------------------
  491. // Private instance methods
  492. //
  493. _restoreBrowser: function _restoreBrowser(browser) {
  494. let vr = this._visibleRect;
  495. if (!seenBrowser(browser))
  496. initBrowserState(browser, vr);
  497. let bvs = getViewportStateFromBrowser(browser);
  498. this._contentWindow = browser.contentWindow;
  499. this._browser = browser;
  500. this._browserViewportState = bvs;
  501. vr.left = bvs.visibleX;
  502. vr.top = bvs.visibleY;
  503. this._tileManager.setBrowser(browser);
  504. },
  505. _viewportChanged: function _viewportChanged(viewportSizeChanged, dirtyAll) {
  506. let bops = this._batchOps;
  507. if (bops.length > 0) {
  508. let opState = bops[bops.length - 1];
  509. if (viewportSizeChanged)
  510. opState.viewportSizeChanged = viewportSizeChanged;
  511. if (dirtyAll)
  512. opState.dirtyAll = dirtyAll;
  513. return;
  514. }
  515. let bvs = this._browserViewportState;
  516. let vis = this._visibleRect;
  517. // !!! --- RESIZE HACK BEGIN -----
  518. // We want to uncomment this for perf, but we can't with the hack in place
  519. // because the mozAfterPaint gives us rects that we use to create the
  520. // fake mozAfterResize event, so we can't just clear things.
  521. /*
  522. if (dirtyAll) {
  523. // We're about to mark the entire viewport dirty, so we can clear any
  524. // queued afterPaint events that will cause redundant draws
  525. getBrowserDOMWindowUtils(this._browser).clearMozAfterPaintEvents();
  526. }
  527. */
  528. // !!! --- RESIZE HACK END -------
  529. if (bvs) {
  530. resizeContainerToViewport(this._container, bvs.viewportRect);
  531. this._tileManager.viewportChangeHandler(bvs.viewportRect,
  532. visibleRectToCriticalRect(vis, bvs),
  533. viewportSizeChanged,
  534. dirtyAll);
  535. }
  536. },
  537. _appendTile: function _appendTile(tile) {
  538. let canvas = tile.getContentImage();
  539. /*
  540. canvas.style.position = "absolute";
  541. canvas.style.left = tile.x + "px";
  542. canvas.style.top = tile.y + "px";
  543. */
  544. canvas.setAttribute("style", "position: absolute; left: " + tile.boundRect.left + "px; " + "top: " + tile.boundRect.top + "px;");
  545. this._container.appendChild(canvas);
  546. //dump('++ ' + tile.toString(true) + endl);
  547. },
  548. _removeTile: function _removeTile(tile) {
  549. let canvas = tile.getContentImage();
  550. this._container.removeChild(canvas);
  551. //dump('-- ' + tile.toString(true) + endl);
  552. }
  553. };
  554. }
  555. )();
  556. // -----------------------------------------------------------
  557. // Helper structures
  558. //
  559. BrowserView.BrowserViewportState = function(viewportRect,
  560. visibleX,
  561. visibleY,
  562. zoomLevel) {
  563. this.init(viewportRect, visibleX, visibleY, zoomLevel);
  564. };
  565. BrowserView.BrowserViewportState.prototype = {
  566. init: function init(viewportRect, visibleX, visibleY, zoomLevel) {
  567. this.viewportRect = viewportRect;
  568. this.visibleX = visibleX;
  569. this.visibleY = visibleY;
  570. this.zoomLevel = zoomLevel;
  571. },
  572. clone: function clone() {
  573. return new BrowserView.BrowserViewportState(this.viewportRect,
  574. this.visibleX,
  575. this.visibleY,
  576. this.zoomLevel);
  577. },
  578. toString: function toString() {
  579. let props = ['\tviewportRect=' + this.viewportRect.toString(),
  580. '\tvisibleX=' + this.visibleX,
  581. '\tvisibleY=' + this.visibleY,
  582. '\tzoomLevel=' + this.zoomLevel];
  583. return '[BrowserViewportState] {\n' + props.join(',\n') + '\n}';
  584. }
  585. };