Revert 285173 "Removed InProcessBrowserTest::CleanUpOnMainThread()"
[chromium-blink-merge.git] / chrome / browser / resources / pdf / viewport.js
blob418ad44071df99f1c4f585d3ed862399e4f6977e
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * Returns the area of the intersection of two rectangles.
7  * @param {Object} rect1 the first rect
8  * @param {Object} rect2 the second rect
9  * @return {number} the area of the intersection of the rects
10  */
11 function getIntersectionArea(rect1, rect2) {
12   var xOverlap = Math.max(0,
13       Math.min(rect1.x + rect1.width, rect2.x + rect2.width) -
14       Math.max(rect1.x, rect2.x));
15   var yOverlap = Math.max(0,
16       Math.min(rect1.y + rect1.height, rect2.y + rect2.height) -
17       Math.max(rect1.y, rect2.y));
18   return xOverlap * yOverlap;
21 /**
22  * Create a new viewport.
23  * @param {Window} window the window
24  * @param {Object} sizer is the element which represents the size of the
25  *     document in the viewport
26  * @param {Function} viewportChangedCallback is run when the viewport changes
27  * @param {Function} beforeZoomCallback is run before a change in zoom
28  * @param {Function} afterZoomCallback is run after a change in zoom
29  * @param {number} scrollbarWidth the width of scrollbars on the page
30  */
31 function Viewport(window,
32                   sizer,
33                   viewportChangedCallback,
34                   beforeZoomCallback,
35                   afterZoomCallback,
36                   scrollbarWidth) {
37   this.window_ = window;
38   this.sizer_ = sizer;
39   this.viewportChangedCallback_ = viewportChangedCallback;
40   this.beforeZoomCallback_ = beforeZoomCallback;
41   this.afterZoomCallback_ = afterZoomCallback;
42   this.allowedToChangeZoom_ = false;
43   this.zoom_ = 1;
44   this.documentDimensions_ = null;
45   this.pageDimensions_ = [];
46   this.scrollbarWidth_ = scrollbarWidth;
47   this.fittingType_ = Viewport.FittingType.NONE;
49   window.addEventListener('scroll', this.updateViewport_.bind(this));
50   window.addEventListener('resize', this.resize_.bind(this));
53 /**
54  * Enumeration of page fitting types.
55  * @enum {string}
56  */
57 Viewport.FittingType = {
58   NONE: 'none',
59   FIT_TO_PAGE: 'fit-to-page',
60   FIT_TO_WIDTH: 'fit-to-width'
63 /**
64  * The increment to scroll a page by in pixels when up/down/left/right arrow
65  * keys are pressed. Usually we just let the browser handle scrolling on the
66  * window when these keys are pressed but in certain cases we need to simulate
67  * these events.
68  */
69 Viewport.SCROLL_INCREMENT = 40;
71 /**
72  * Predefined zoom factors to be used when zooming in/out. These are in
73  * ascending order.
74  */
75 Viewport.ZOOM_FACTORS = [0.25, 0.333, 0.5, 0.666, 0.75, 0.9, 1,
76                          1.1, 1.25, 1.5, 1.75, 2, 2.5, 3, 4, 5];
78 /**
79  * The width of the page shadow around pages in pixels.
80  */
81 Viewport.PAGE_SHADOW = {top: 3, bottom: 7, left: 5, right: 5};
83 Viewport.prototype = {
84   /**
85    * @private
86    * Returns true if the document needs scrollbars at the given zoom level.
87    * @param {number} zoom compute whether scrollbars are needed at this zoom
88    * @return {Object} with 'horizontal' and 'vertical' keys which map to bool
89    *     values indicating if the horizontal and vertical scrollbars are needed
90    *     respectively.
91    */
92   documentNeedsScrollbars_: function(zoom) {
93     if (!this.documentDimensions_) {
94       return {
95         horizontal: false,
96         vertical: false
97       };
98     }
99     var documentWidth = this.documentDimensions_.width * zoom;
100     var documentHeight = this.documentDimensions_.height * zoom;
101     return {
102       horizontal: documentWidth > this.window_.innerWidth,
103       vertical: documentHeight > this.window_.innerHeight
104     };
105   },
107   /**
108    * Returns true if the document needs scrollbars at the current zoom level.
109    * @return {Object} with 'x' and 'y' keys which map to bool values
110    *     indicating if the horizontal and vertical scrollbars are needed
111    *     respectively.
112    */
113   documentHasScrollbars: function() {
114     return this.documentNeedsScrollbars_(this.zoom_);
115   },
117   /**
118    * @private
119    * Helper function called when the zoomed document size changes.
120    */
121   contentSizeChanged_: function() {
122     if (this.documentDimensions_) {
123       this.sizer_.style.width =
124           this.documentDimensions_.width * this.zoom_ + 'px';
125       this.sizer_.style.height =
126           this.documentDimensions_.height * this.zoom_ + 'px';
127     }
128   },
130   /**
131    * @private
132    * Called when the viewport should be updated.
133    */
134   updateViewport_: function() {
135     this.viewportChangedCallback_();
136   },
138   /**
139    * @private
140    * Called when the viewport size changes.
141    */
142   resize_: function() {
143     if (this.fittingType_ == Viewport.FittingType.FIT_TO_PAGE)
144       this.fitToPage();
145     else if (this.fittingType_ == Viewport.FittingType.FIT_TO_WIDTH)
146       this.fitToWidth();
147     else
148       this.updateViewport_();
149   },
151   /**
152    * @type {Object} the scroll position of the viewport.
153    */
154   get position() {
155     return {
156       x: this.window_.pageXOffset,
157       y: this.window_.pageYOffset
158     };
159   },
161   /**
162    * Scroll the viewport to the specified position.
163    * @type {Object} position the position to scroll to.
164    */
165   set position(position) {
166     this.window_.scrollTo(position.x, position.y);
167   },
169   /**
170    * @type {Object} the size of the viewport excluding scrollbars.
171    */
172   get size() {
173     var needsScrollbars = this.documentNeedsScrollbars_(this.zoom_);
174     var scrollbarWidth = needsScrollbars.vertical ? this.scrollbarWidth_ : 0;
175     var scrollbarHeight = needsScrollbars.horizontal ? this.scrollbarWidth_ : 0;
176     return {
177       width: this.window_.innerWidth - scrollbarWidth,
178       height: this.window_.innerHeight - scrollbarHeight
179     };
180   },
182   /**
183    * @type {number} the zoom level of the viewport.
184    */
185   get zoom() {
186     return this.zoom_;
187   },
189   /**
190    * @private
191    * Used to wrap a function that might perform zooming on the viewport. This is
192    * required so that we can notify the plugin that zooming is in progress
193    * so that while zooming is taking place it can stop reacting to scroll events
194    * from the viewport. This is to avoid flickering.
195    */
196   mightZoom_: function(f) {
197     this.beforeZoomCallback_();
198     this.allowedToChangeZoom_ = true;
199     f();
200     this.allowedToChangeZoom_ = false;
201     this.afterZoomCallback_();
202   },
204   /**
205    * @private
206    * Sets the zoom of the viewport.
207    * @param {number} newZoom the zoom level to zoom to.
208    */
209   setZoomInternal_: function(newZoom) {
210     if (!this.allowedToChangeZoom_) {
211       throw 'Called Viewport.setZoomInternal_ without calling ' +
212             'Viewport.mightZoom_.';
213     }
214     var oldZoom = this.zoom_;
215     this.zoom_ = newZoom;
216     // Record the scroll position (relative to the middle of the window).
217     var currentScrollPos = [
218       (this.window_.pageXOffset + this.window_.innerWidth / 2) / oldZoom,
219       (this.window_.pageYOffset + this.window_.innerHeight / 2) / oldZoom
220     ];
221     this.contentSizeChanged_();
222     // Scroll to the scaled scroll position.
223     this.window_.scrollTo(
224         currentScrollPos[0] * newZoom - this.window_.innerWidth / 2,
225         currentScrollPos[1] * newZoom - this.window_.innerHeight / 2);
226   },
228   /**
229    * Sets the zoom to the given zoom level.
230    * @param {number} newZoom the zoom level to zoom to.
231    */
232   setZoom: function(newZoom) {
233     this.mightZoom_(function() {
234       this.setZoomInternal_(newZoom);
235       this.updateViewport_();
236     }.bind(this));
237   },
239   /**
240    * @type {number} the width of scrollbars in the viewport in pixels.
241    */
242   get scrollbarWidth() {
243     return this.scrollbarWidth_;
244   },
246   /**
247    * @type {Viewport.FittingType} the fitting type the viewport is currently in.
248    */
249   get fittingType() {
250     return this.fittingType_;
251   },
253   /**
254    * @private
255    * @param {integer} y the y-coordinate to get the page at.
256    * @return {integer} the index of a page overlapping the given y-coordinate.
257    */
258   getPageAtY_: function(y) {
259     var min = 0;
260     var max = this.pageDimensions_.length - 1;
261     while (max >= min) {
262       var page = Math.floor(min + ((max - min) / 2));
263       // There might be a gap between the pages, in which case use the bottom
264       // of the previous page as the top for finding the page.
265       var top = 0;
266       if (page > 0) {
267         top = this.pageDimensions_[page - 1].y +
268             this.pageDimensions_[page - 1].height;
269       }
270       var bottom = this.pageDimensions_[page].y +
271           this.pageDimensions_[page].height;
273       if (top <= y && bottom > y)
274         return page;
275       else if (top > y)
276         max = page - 1;
277       else
278         min = page + 1;
279     }
280     return 0;
281   },
283   /**
284    * Returns the page with the most pixels in the current viewport.
285    * @return {int} the index of the most visible page.
286    */
287   getMostVisiblePage: function() {
288     var firstVisiblePage = this.getPageAtY_(this.position.y / this.zoom_);
289     var mostVisiblePage = {number: 0, area: 0};
290     var viewportRect = {
291       x: this.position.x / this.zoom_,
292       y: this.position.y / this.zoom_,
293       width: this.size.width / this.zoom_,
294       height: this.size.height / this.zoom_
295     };
296     for (var i = firstVisiblePage; i < this.pageDimensions_.length; i++) {
297       var area = getIntersectionArea(this.pageDimensions_[i],
298                                      viewportRect);
299       // If we hit a page with 0 area overlap, we must have gone past the
300       // pages visible in the viewport so we can break.
301       if (area == 0)
302         break;
303       if (area > mostVisiblePage.area) {
304         mostVisiblePage.area = area;
305         mostVisiblePage.number = i;
306       }
307     }
308     return mostVisiblePage.number;
309   },
311   /**
312    * @private
313    * Compute the zoom level for fit-to-page or fit-to-width. |pageDimensions| is
314    * the dimensions for a given page and if |widthOnly| is true, it indicates
315    * that fit-to-page zoom should be computed rather than fit-to-page.
316    * @param {Object} pageDimensions the dimensions of a given page
317    * @param {boolean} widthOnly a bool indicating whether fit-to-page or
318    *     fit-to-width should be computed.
319    * @return {number} the zoom to use
320    */
321   computeFittingZoom_: function(pageDimensions, widthOnly) {
322     // First compute the zoom without scrollbars.
323     var zoomWidth = this.window_.innerWidth / pageDimensions.width;
324     var zoom;
325     if (widthOnly) {
326       zoom = zoomWidth;
327     } else {
328       var zoomHeight = this.window_.innerHeight / pageDimensions.height;
329       zoom = Math.min(zoomWidth, zoomHeight);
330     }
331     // Check if there needs to be any scrollbars.
332     var needsScrollbars = this.documentNeedsScrollbars_(zoom);
334     // If the document fits, just return the zoom.
335     if (!needsScrollbars.horizontal && !needsScrollbars.vertical)
336       return zoom;
338     var zoomedDimensions = {
339       width: this.documentDimensions_.width * zoom,
340       height: this.documentDimensions_.height * zoom
341     };
343     // Check if adding a scrollbar will result in needing the other scrollbar.
344     var scrollbarWidth = this.scrollbarWidth_;
345     if (needsScrollbars.horizontal &&
346         zoomedDimensions.height > this.window_.innerHeight - scrollbarWidth) {
347       needsScrollbars.vertical = true;
348     }
349     if (needsScrollbars.vertical &&
350         zoomedDimensions.width > this.window_.innerWidth - scrollbarWidth) {
351       needsScrollbars.horizontal = true;
352     }
354     // Compute available window space.
355     var windowWithScrollbars = {
356       width: this.window_.innerWidth,
357       height: this.window_.innerHeight
358     };
359     if (needsScrollbars.horizontal)
360       windowWithScrollbars.height -= scrollbarWidth;
361     if (needsScrollbars.vertical)
362       windowWithScrollbars.width -= scrollbarWidth;
364     // Recompute the zoom.
365     zoomWidth = windowWithScrollbars.width / pageDimensions.width;
366     if (widthOnly) {
367       zoom = zoomWidth;
368     } else {
369       var zoomHeight = windowWithScrollbars.height / pageDimensions.height;
370       zoom = Math.min(zoomWidth, zoomHeight);
371     }
372     return zoom;
373   },
375   /**
376    * Zoom the viewport so that the page-width consumes the entire viewport.
377    */
378   fitToWidth: function() {
379     this.mightZoom_(function() {
380       this.fittingType_ = Viewport.FittingType.FIT_TO_WIDTH;
381       if (!this.documentDimensions_)
382         return;
383       // Track the last y-position to stay at the same position after zooming.
384       var oldY = this.window_.pageYOffset / this.zoom_;
385       // When computing fit-to-width, the maximum width of a page in the
386       // document is used, which is equal to the size of the document width.
387       this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_,
388                                                      true));
389       var page = this.getMostVisiblePage();
390       this.window_.scrollTo(0, oldY * this.zoom_);
391       this.updateViewport_();
392     }.bind(this));
393   },
395   /**
396    * Zoom the viewport so that a page consumes the entire viewport. Also scrolls
397    * to the top of the most visible page.
398    */
399   fitToPage: function() {
400     this.mightZoom_(function() {
401       this.fittingType_ = Viewport.FittingType.FIT_TO_PAGE;
402       if (!this.documentDimensions_)
403         return;
404       var page = this.getMostVisiblePage();
405       this.setZoomInternal_(this.computeFittingZoom_(
406           this.pageDimensions_[page], false));
407       // Center the document in the page by scrolling by the amount of empty
408       // space to the left of the document.
409       var xOffset =
410           (this.documentDimensions_.width - this.pageDimensions_[page].width) *
411           this.zoom_ / 2;
412       this.window_.scrollTo(xOffset,
413                             this.pageDimensions_[page].y * this.zoom_);
414       this.updateViewport_();
415     }.bind(this));
416   },
418   /**
419    * Zoom out to the next predefined zoom level.
420    */
421   zoomOut: function() {
422     this.mightZoom_(function() {
423       this.fittingType_ = Viewport.FittingType.NONE;
424       var nextZoom = Viewport.ZOOM_FACTORS[0];
425       for (var i = 0; i < Viewport.ZOOM_FACTORS.length; i++) {
426         if (Viewport.ZOOM_FACTORS[i] < this.zoom_)
427           nextZoom = Viewport.ZOOM_FACTORS[i];
428       }
429       this.setZoomInternal_(nextZoom);
430       this.updateViewport_();
431     }.bind(this));
432   },
434   /**
435    * Zoom in to the next predefined zoom level.
436    */
437   zoomIn: function() {
438     this.mightZoom_(function() {
439       this.fittingType_ = Viewport.FittingType.NONE;
440       var nextZoom = Viewport.ZOOM_FACTORS[Viewport.ZOOM_FACTORS.length - 1];
441       for (var i = Viewport.ZOOM_FACTORS.length - 1; i >= 0; i--) {
442         if (Viewport.ZOOM_FACTORS[i] > this.zoom_)
443           nextZoom = Viewport.ZOOM_FACTORS[i];
444       }
445       this.setZoomInternal_(nextZoom);
446       this.updateViewport_();
447     }.bind(this));
448   },
450   /**
451    * Go to the given page index.
452    * @param {number} page the index of the page to go to.
453    */
454   goToPage: function(page) {
455     this.mightZoom_(function() {
456       if (this.pageDimensions_.length == 0)
457         return;
458       if (page < 0)
459         page = 0;
460       if (page >= this.pageDimensions_.length)
461         page = this.pageDimensions_.length - 1;
462       var dimensions = this.pageDimensions_[page];
463       this.window_.scrollTo(dimensions.x * this.zoom_,
464                             dimensions.y * this.zoom_);
465       this.updateViewport_();
466     }.bind(this));
467   },
469   /**
470    * Set the dimensions of the document.
471    * @param {Object} documentDimensions the dimensions of the document
472    */
473   setDocumentDimensions: function(documentDimensions) {
474     this.mightZoom_(function() {
475       var initialDimensions = !this.documentDimensions_;
476       this.documentDimensions_ = documentDimensions;
477       this.pageDimensions_ = this.documentDimensions_.pageDimensions;
478       if (initialDimensions) {
479         this.setZoomInternal_(this.computeFittingZoom_(this.documentDimensions_,
480                                                        true));
481         if (this.zoom_ > 1)
482           this.setZoomInternal_(1);
483         this.window_.scrollTo(0, 0);
484       }
485       this.contentSizeChanged_();
486       this.resize_();
487     }.bind(this));
488   },
490   /**
491    * Get the coordinates of the page contents (excluding the page shadow)
492    * relative to the screen.
493    * @param {number} page the index of the page to get the rect for.
494    * @return {Object} a rect representing the page in screen coordinates.
495    */
496   getPageScreenRect: function(page) {
497     if (!this.documentDimensions_) {
498       return {
499         x: 0,
500         y: 0,
501         width: 0,
502         height: 0
503       };
504     }
505     if (page >= this.pageDimensions_.length)
506       page = this.pageDimensions_.length - 1;
508     var pageDimensions = this.pageDimensions_[page];
510     // Compute the page dimensions minus the shadows.
511     var insetDimensions = {
512       x: pageDimensions.x + Viewport.PAGE_SHADOW.left,
513       y: pageDimensions.y + Viewport.PAGE_SHADOW.top,
514       width: pageDimensions.width - Viewport.PAGE_SHADOW.left -
515           Viewport.PAGE_SHADOW.right,
516       height: pageDimensions.height - Viewport.PAGE_SHADOW.top -
517           Viewport.PAGE_SHADOW.bottom
518     };
520     // Compute the x-coordinate of the page within the document.
521     // TODO(raymes): This should really be set when the PDF plugin passes the
522     // page coordinates, but it isn't yet.
523     var x = (this.documentDimensions_.width - pageDimensions.width) / 2 +
524         Viewport.PAGE_SHADOW.left;
525     // Compute the space on the left of the document if the document fits
526     // completely in the screen.
527     var spaceOnLeft = (this.size.width -
528         this.documentDimensions_.width * this.zoom_) / 2;
529     spaceOnLeft = Math.max(spaceOnLeft, 0);
531     return {
532       x: x * this.zoom_ + spaceOnLeft - this.window_.pageXOffset,
533       y: insetDimensions.y * this.zoom_ - this.window_.pageYOffset,
534       width: insetDimensions.width * this.zoom_,
535       height: insetDimensions.height * this.zoom_
536     };
537   }