Home | History | Annotate | Download | only in cocoa
      1 // Copyright (c) 2012 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.
      4 
      5 #import "chrome/browser/ui/cocoa/tabpose_window.h"
      6 
      7 #import <QuartzCore/QuartzCore.h>
      8 
      9 #include <algorithm>
     10 
     11 #include "base/mac/mac_util.h"
     12 #include "base/mac/scoped_cftyperef.h"
     13 #include "base/memory/weak_ptr.h"
     14 #include "base/prefs/pref_service.h"
     15 #include "base/strings/sys_string_conversions.h"
     16 #include "chrome/app/chrome_command_ids.h"
     17 #include "chrome/browser/browser_process.h"
     18 #include "chrome/browser/devtools/devtools_window.h"
     19 #include "chrome/browser/extensions/tab_helper.h"
     20 #include "chrome/browser/profiles/profile.h"
     21 #include "chrome/browser/thumbnails/render_widget_snapshot_taker.h"
     22 #include "chrome/browser/ui/bookmarks/bookmark_tab_helper.h"
     23 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h"
     24 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
     25 #import "chrome/browser/ui/cocoa/infobars/infobar_container_controller.h"
     26 #import "chrome/browser/ui/cocoa/tab_contents/favicon_util_mac.h"
     27 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
     28 #import "chrome/browser/ui/cocoa/tabs/tab_strip_model_observer_bridge.h"
     29 #include "chrome/common/pref_names.h"
     30 #include "content/public/browser/browser_thread.h"
     31 #include "content/public/browser/render_view_host.h"
     32 #include "content/public/browser/render_widget_host_view.h"
     33 #include "content/public/browser/web_contents.h"
     34 #include "content/public/browser/web_contents_view.h"
     35 #include "grit/theme_resources.h"
     36 #include "grit/ui_resources.h"
     37 #include "skia/ext/skia_utils_mac.h"
     38 #include "third_party/skia/include/utils/mac/SkCGUtils.h"
     39 #include "ui/base/cocoa/animation_utils.h"
     40 #include "ui/base/resource/resource_bundle.h"
     41 #include "ui/gfx/image/image.h"
     42 #include "ui/gfx/scoped_cg_context_save_gstate_mac.h"
     43 
     44 using content::BrowserThread;
     45 using content::RenderWidgetHost;
     46 
     47 // Height of the bottom gradient, in pixels.
     48 const CGFloat kBottomGradientHeight = 50;
     49 
     50 // The shade of gray at the top of the window. There's a  gradient from
     51 // this to |kCentralGray| at the top of the window.
     52 const CGFloat kTopGray = 0.77;
     53 
     54 // The shade of gray at the center of the window. Most of the window background
     55 // has this color.
     56 const CGFloat kCentralGray = 0.6;
     57 
     58 // The shade of gray at the bottom of the window. There's a gradient from
     59 // |kCentralGray| to this at the bottom of the window, |kBottomGradientHeight|
     60 // high.
     61 const CGFloat kBottomGray = 0.5;
     62 
     63 NSString* const kAnimationIdKey = @"AnimationId";
     64 NSString* const kAnimationIdFadeIn = @"FadeIn";
     65 NSString* const kAnimationIdFadeOut = @"FadeOut";
     66 
     67 const CGFloat kDefaultAnimationDuration = 0.25;  // In seconds.
     68 const CGFloat kSlomoFactor = 4;
     69 const CGFloat kObserverChangeAnimationDuration = 0.25;  // In seconds.
     70 const CGFloat kSelectionInset = 5;
     71 
     72 // CAGradientLayer is 10.6-only -- roll our own.
     73 @interface GrayGradientLayer : CALayer {
     74  @private
     75   CGFloat startGray_;
     76   CGFloat endGray_;
     77 }
     78 - (id)initWithStartGray:(CGFloat)startGray endGray:(CGFloat)endGray;
     79 - (void)drawInContext:(CGContextRef)context;
     80 @end
     81 
     82 @implementation GrayGradientLayer
     83 - (id)initWithStartGray:(CGFloat)startGray endGray:(CGFloat)endGray {
     84   if ((self = [super init])) {
     85     startGray_ = startGray;
     86     endGray_ = endGray;
     87   }
     88   return self;
     89 }
     90 
     91 - (void)drawInContext:(CGContextRef)context {
     92   base::ScopedCFTypeRef<CGColorSpaceRef> grayColorSpace(
     93       CGColorSpaceCreateWithName(kCGColorSpaceGenericGray));
     94   CGFloat grays[] = { startGray_, 1.0, endGray_, 1.0 };
     95   CGFloat locations[] = { 0, 1 };
     96   base::ScopedCFTypeRef<CGGradientRef> gradient(
     97       CGGradientCreateWithColorComponents(
     98           grayColorSpace.get(), grays, locations, arraysize(locations)));
     99   CGPoint topLeft = CGPointMake(0.0, self.bounds.size.height);
    100   CGContextDrawLinearGradient(context, gradient.get(), topLeft, CGPointZero, 0);
    101 }
    102 @end
    103 
    104 namespace tabpose {
    105 class ThumbnailLoader;
    106 }
    107 
    108 // A CALayer that draws a thumbnail for a WebContents object. The layer
    109 // tries to draw the WebContents's backing store directly if possible, and
    110 // requests a thumbnail bitmap from the WebContents's renderer process if not.
    111 @interface ThumbnailLayer : CALayer {
    112   // The WebContents the thumbnail is for.
    113   content::WebContents* contents_;  // weak
    114 
    115   // The size the thumbnail is drawn at when zoomed in.
    116   NSSize fullSize_;
    117 
    118   // Used to load a thumbnail, if required.
    119   scoped_refptr<tabpose::ThumbnailLoader> loader_;
    120 
    121   // If the backing store couldn't be used and a thumbnail was returned from a
    122   // renderer process, it's stored in |thumbnail_|.
    123   base::ScopedCFTypeRef<CGImageRef> thumbnail_;
    124 
    125   // True if the layer already sent a thumbnail request to a renderer.
    126   BOOL didSendLoad_;
    127 }
    128 - (id)initWithWebContents:(content::WebContents*)contents
    129                  fullSize:(NSSize)fullSize;
    130 - (void)drawInContext:(CGContextRef)context;
    131 - (void)setThumbnail:(const SkBitmap&)bitmap;
    132 @end
    133 
    134 namespace tabpose {
    135 
    136 // ThumbnailLoader talks to the renderer process to load a thumbnail of a given
    137 // RenderWidgetHost, and sends the thumbnail back to a ThumbnailLayer once it
    138 // comes back from the renderer.
    139 class ThumbnailLoader : public base::RefCountedThreadSafe<ThumbnailLoader> {
    140  public:
    141   ThumbnailLoader(gfx::Size size, RenderWidgetHost* rwh, ThumbnailLayer* layer)
    142       : size_(size), rwh_(rwh), layer_(layer), weak_factory_(this) {}
    143 
    144   // Starts the fetch.
    145   void LoadThumbnail();
    146 
    147  private:
    148   friend class base::RefCountedThreadSafe<ThumbnailLoader>;
    149   ~ThumbnailLoader() {
    150   }
    151 
    152   void DidReceiveBitmap(const SkBitmap& bitmap) {
    153     DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    154     [layer_ setThumbnail:bitmap];
    155   }
    156 
    157   gfx::Size size_;
    158   RenderWidgetHost* rwh_;  // weak
    159   ThumbnailLayer* layer_;  // weak, owns us
    160   base::WeakPtrFactory<ThumbnailLoader> weak_factory_;
    161 
    162   DISALLOW_COPY_AND_ASSIGN(ThumbnailLoader);
    163 };
    164 
    165 void ThumbnailLoader::LoadThumbnail() {
    166   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    167 
    168   // As mentioned in ThumbnailLayer's -drawInContext:, it's sufficient to have
    169   // thumbnails at the zoomed-out pixel size for all but the thumbnail the user
    170   // clicks on in the end. But we don't don't which thumbnail that will be, so
    171   // keep it simple and request full thumbnails for everything.
    172   // TODO(thakis): Request smaller thumbnails for users with many tabs.
    173   gfx::Size page_size(size_);  // Logical size the renderer renders at.
    174   gfx::Size pixel_size(size_);  // Physical pixel size the image is rendered at.
    175 
    176   // Will send an IPC to the renderer on the IO thread.
    177   g_browser_process->GetRenderWidgetSnapshotTaker()->AskForSnapshot(
    178       rwh_,
    179       base::Bind(&ThumbnailLoader::DidReceiveBitmap,
    180                  weak_factory_.GetWeakPtr()),
    181       page_size,
    182       pixel_size);
    183 }
    184 
    185 }  // namespace tabpose
    186 
    187 @implementation ThumbnailLayer
    188 
    189 - (id)initWithWebContents:(content::WebContents*)contents
    190                  fullSize:(NSSize)fullSize {
    191   CHECK(contents);
    192   if ((self = [super init])) {
    193     contents_ = contents;
    194     fullSize_ = fullSize;
    195   }
    196   return self;
    197 }
    198 
    199 - (void)setWebContents:(content::WebContents*)contents {
    200   contents_ = contents;
    201 }
    202 
    203 - (void)setThumbnail:(const SkBitmap&)bitmap {
    204   // SkCreateCGImageRef() holds on to |bitmaps|'s memory, so this doesn't
    205   // create a copy. The renderer always draws data in the system colorspace.
    206   thumbnail_.reset(SkCreateCGImageRefWithColorspace(
    207       bitmap, base::mac::GetSystemColorSpace()));
    208   loader_ = NULL;
    209   [self setNeedsDisplay];
    210 }
    211 
    212 - (int)topOffset {
    213   int topOffset = 0;
    214 
    215   // Medium term, we want to show thumbs of the actual info bar views, which
    216   // means I need to create InfoBarControllers here.
    217   NSWindow* window = [contents_->GetView()->GetNativeView() window];
    218   NSWindowController* windowController = [window windowController];
    219   if ([windowController isKindOfClass:[BrowserWindowController class]]) {
    220     BrowserWindowController* bwc =
    221         static_cast<BrowserWindowController*>(windowController);
    222     InfoBarContainerController* infoBarContainer =
    223         [bwc infoBarContainerController];
    224     // TODO(thakis|rsesek): This is not correct for background tabs with
    225     // infobars as the aspect ratio will be wrong. Fix that.
    226     topOffset += NSHeight([[infoBarContainer view] frame]) -
    227         [infoBarContainer overlappingTipHeight];
    228   }
    229 
    230   BookmarkTabHelper* bookmark_tab_helper =
    231       BookmarkTabHelper::FromWebContents(contents_);
    232   Profile* profile =
    233       Profile::FromBrowserContext(contents_->GetBrowserContext());
    234   bool always_show_bookmark_bar =
    235       profile->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar);
    236   bool has_detached_bookmark_bar =
    237       bookmark_tab_helper->ShouldShowBookmarkBar() &&
    238       !always_show_bookmark_bar;
    239   if (has_detached_bookmark_bar)
    240     topOffset += chrome::kNTPBookmarkBarHeight;
    241 
    242   return topOffset;
    243 }
    244 
    245 - (int)bottomOffset {
    246   int bottomOffset = 0;
    247   DevToolsWindow* devToolsWindow =
    248       DevToolsWindow::GetDockedInstanceForInspectedTab(contents_);
    249   content::WebContents* devToolsContents =
    250       devToolsWindow ? devToolsWindow->web_contents() : NULL;
    251   if (devToolsContents && devToolsContents->GetRenderViewHost() &&
    252       devToolsContents->GetRenderViewHost()->GetView()) {
    253     // The devtool's size might not be up-to-date, but since its height doesn't
    254     // change on window resize, and since most users don't use devtools, this is
    255     // good enough.
    256     bottomOffset += devToolsContents->GetRenderViewHost()->GetView()->
    257         GetViewBounds().height();
    258     bottomOffset += 1;  // :-( Divider line between web contents and devtools.
    259   }
    260   return bottomOffset;
    261 }
    262 
    263 - (void)drawInContext:(CGContextRef)context {
    264   RenderWidgetHost* rwh = contents_->GetRenderViewHost();
    265   // NULL if renderer crashed.
    266   content::RenderWidgetHostView* rwhv = rwh ? rwh->GetView() : NULL;
    267   if (!rwhv) {
    268     // TODO(thakis): Maybe draw a sad tab layer?
    269     [super drawInContext:context];
    270     return;
    271   }
    272 
    273   // The size of the WebContents's RenderWidgetHost might not fit to the
    274   // current browser window at all, for example if the window was resized while
    275   // this WebContents object was not an active tab.
    276   // Compute the required size ourselves. Leave room for eventual infobars and
    277   // a detached bookmarks bar on the top, and for the devtools on the bottom.
    278   // Download shelf is not included in the |fullSize| rect, so no need to
    279   // correct for it here.
    280   // TODO(thakis): This is not resolution-independent.
    281   int topOffset = [self topOffset];
    282   int bottomOffset = [self bottomOffset];
    283   gfx::Size desiredThumbSize(fullSize_.width,
    284                              fullSize_.height - topOffset - bottomOffset);
    285 
    286   // We need to ask the renderer for a thumbnail if
    287   // a) there's no backing store or
    288   // b) the backing store's size doesn't match our required size and
    289   // c) we didn't already send a thumbnail request to the renderer.
    290   bool draw_backing_store = rwh->GetBackingStoreSize() == desiredThumbSize;
    291 
    292   // Next weirdness: The destination rect. If the layer is |fullSize_| big, the
    293   // destination rect is (0, bottomOffset), (fullSize_.width, topOffset). But we
    294   // might be amidst an animation, so interpolate that rect.
    295   CGRect destRect = [self bounds];
    296   CGFloat scale = destRect.size.width / fullSize_.width;
    297   destRect.origin.y += bottomOffset * scale;
    298   destRect.size.height -= (bottomOffset + topOffset) * scale;
    299 
    300   // TODO(thakis): Draw infobars, detached bookmark bar as well.
    301 
    302   // If we haven't already, sent a thumbnail request to the renderer.
    303   if (!draw_backing_store && !didSendLoad_) {
    304     // Either the tab was never visible, or its backing store got evicted, or
    305     // the size of the backing store is wrong.
    306 
    307     // We only need a thumbnail the size of the zoomed-out layer for all
    308     // layers except the one the user clicks on. But since we can't know which
    309     // layer that is, request full-resolution layers for all tabs. This is
    310     // simple and seems to work in practice.
    311     loader_ = new tabpose::ThumbnailLoader(desiredThumbSize, rwh, self);
    312     loader_->LoadThumbnail();
    313     didSendLoad_ = YES;
    314 
    315     // Fill with bg color.
    316     [super drawInContext:context];
    317   }
    318 
    319   if (draw_backing_store) {
    320     // Backing store 'cache' hit!
    321     // TODO(thakis): Add a sublayer for each accelerated surface in the rwhv.
    322     // Until then, accelerated layers (CoreAnimation NPAPI plugins, compositor)
    323     // won't show up in tabpose.
    324     rwh->CopyFromBackingStoreToCGContext(destRect, context);
    325   } else if (thumbnail_) {
    326     // No cache hit, but the renderer returned a thumbnail to us.
    327     gfx::ScopedCGContextSaveGState save_gstate(context);
    328     CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
    329     CGContextDrawImage(context, destRect, thumbnail_.get());
    330   }
    331 }
    332 
    333 @end
    334 
    335 // Given the number |n| of tiles with a desired aspect ratio of |a| and a
    336 // desired distance |dx|, |dy| between tiles, returns how many tiles fit
    337 // vertically into a rectangle with the dimensions |w_c|, |h_c|. This returns
    338 // an exact solution, which is usually a fractional number.
    339 static float FitNRectsWithAspectIntoBoundingSizeWithConstantPadding(
    340     int n, double a, int w_c, int h_c, int dx, int dy) {
    341   // We want to have the small rects have the same aspect ratio a as a full
    342   // tab. Let w, h be the size of a small rect, and w_c, h_c the size of the
    343   // container. dx, dy are the distances between small rects in x, y direction.
    344 
    345   // Geometry yields:
    346   // w_c = nx * (w + dx) - dx <=> w = (w_c + d_x) / nx - d_x
    347   // h_c = ny * (h + dy) - dy <=> h = (h_c + d_y) / ny - d_t
    348   // Plugging this into
    349   // a := tab_width / tab_height = w / h
    350   // yields
    351   // a = ((w_c - (nx - 1)*d_x)*ny) / (nx*(h_c - (ny - 1)*d_y))
    352   // Plugging in nx = n/ny and pen and paper (or wolfram alpha:
    353   // http://www.wolframalpha.com/input/?i=(-sqrt((d+n-a+f+n)^2-4+(a+f%2Ba+h)+(-d+n-n+w))%2Ba+f+n-d+n)/(2+a+(f%2Bh)) , (solution for nx)
    354   // http://www.wolframalpha.com/input/?i=+(-sqrt((a+f+n-d+n)^2-4+(d%2Bw)+(-a+f+n-a+h+n))-a+f+n%2Bd+n)/(2+(d%2Bw)) , (solution for ny)
    355   // ) gives us nx and ny (but the wrong root -- s/-sqrt(FOO)/sqrt(FOO)/.
    356 
    357   // This function returns ny.
    358   return (sqrt(pow(n * (a * dy - dx), 2) +
    359                4 * n * a * (dx + w_c) * (dy + h_c)) -
    360           n * (a * dy - dx))
    361       /
    362          (2 * (dx + w_c));
    363 }
    364 
    365 namespace tabpose {
    366 
    367 CGFloat ScaleWithOrigin(CGFloat x, CGFloat origin, CGFloat scale) {
    368   return (x - origin) * scale + origin;
    369 }
    370 
    371 NSRect ScaleRectWithOrigin(NSRect r, NSPoint p, CGFloat scale) {
    372   return NSMakeRect(ScaleWithOrigin(NSMinX(r), p.x, scale),
    373                     ScaleWithOrigin(NSMinY(r), p.y, scale),
    374                     NSWidth(r) * scale,
    375                     NSHeight(r) * scale);
    376 }
    377 
    378 // A tile is what is shown for a single tab in tabpose mode. It consists of a
    379 // title, favicon, thumbnail image, and pre- and postanimation rects.
    380 class Tile {
    381  public:
    382   Tile() {}
    383 
    384   // Returns the rectangle this thumbnail is at at the beginning of the zoom-in
    385   // animation. |tile| is the rectangle that's covering the whole tab area when
    386   // the animation starts.
    387   NSRect GetStartRectRelativeTo(const Tile& tile) const;
    388   NSRect thumb_rect() const { return thumb_rect_; }
    389 
    390   NSRect GetFaviconStartRectRelativeTo(const Tile& tile) const;
    391   NSRect favicon_rect() const { return NSIntegralRect(favicon_rect_); }
    392   NSImage* favicon() const;
    393 
    394   // This changes |title_rect| and |favicon_rect| such that the favicon is on
    395   // the font's baseline and that the minimum distance between thumb rect and
    396   // favicon and title rects doesn't change.
    397   // The view code
    398   // 1. queries desired font size by calling |title_font_size()|
    399   // 2. loads that font
    400   // 3. calls |set_font_metrics()| which updates the title rect
    401   // 4. receives the title rect and puts the title on it with the font from 2.
    402   void set_font_metrics(CGFloat ascender, CGFloat descender);
    403   CGFloat title_font_size() const { return title_font_size_; }
    404 
    405   NSRect GetTitleStartRectRelativeTo(const Tile& tile) const;
    406   NSRect title_rect() const { return NSIntegralRect(title_rect_); }
    407 
    408   // Returns an unelided title. The view logic is responsible for eliding.
    409   const base::string16& title() const {
    410     return contents_->GetTitle();
    411   }
    412 
    413   content::WebContents* web_contents() const { return contents_; }
    414   void set_tab_contents(content::WebContents* new_contents) {
    415     contents_ = new_contents;
    416   }
    417 
    418  private:
    419   friend class TileSet;
    420 
    421   // The thumb rect includes infobars, detached thumbnail bar, web contents,
    422   // and devtools.
    423   NSRect thumb_rect_;
    424   NSRect start_thumb_rect_;
    425 
    426   NSRect favicon_rect_;
    427 
    428   CGFloat title_font_size_;
    429   NSRect title_rect_;
    430 
    431   content::WebContents* contents_;  // weak
    432 
    433   DISALLOW_COPY_AND_ASSIGN(Tile);
    434 };
    435 
    436 NSRect Tile::GetStartRectRelativeTo(const Tile& tile) const {
    437   NSRect rect = start_thumb_rect_;
    438   rect.origin.x -= tile.start_thumb_rect_.origin.x;
    439   rect.origin.y -= tile.start_thumb_rect_.origin.y;
    440   return rect;
    441 }
    442 
    443 NSRect Tile::GetFaviconStartRectRelativeTo(const Tile& tile) const {
    444   NSRect thumb_start = GetStartRectRelativeTo(tile);
    445   CGFloat scale_to_start = NSWidth(thumb_start) / NSWidth(thumb_rect_);
    446   NSRect rect =
    447       ScaleRectWithOrigin(favicon_rect_, thumb_rect_.origin, scale_to_start);
    448   rect.origin.x += NSMinX(thumb_start) - NSMinX(thumb_rect_);
    449   rect.origin.y += NSMinY(thumb_start) - NSMinY(thumb_rect_);
    450   return rect;
    451 }
    452 
    453 NSImage* Tile::favicon() const {
    454   extensions::TabHelper* extensions_tab_helper =
    455       extensions::TabHelper::FromWebContents(contents_);
    456   if (extensions_tab_helper->is_app()) {
    457     SkBitmap* bitmap = extensions_tab_helper->GetExtensionAppIcon();
    458     if (bitmap)
    459       return gfx::SkBitmapToNSImage(*bitmap);
    460   }
    461   return mac::FaviconForWebContents(contents_);
    462 }
    463 
    464 NSRect Tile::GetTitleStartRectRelativeTo(const Tile& tile) const {
    465   NSRect thumb_start = GetStartRectRelativeTo(tile);
    466   CGFloat scale_to_start = NSWidth(thumb_start) / NSWidth(thumb_rect_);
    467   NSRect rect =
    468       ScaleRectWithOrigin(title_rect_, thumb_rect_.origin, scale_to_start);
    469   rect.origin.x += NSMinX(thumb_start) - NSMinX(thumb_rect_);
    470   rect.origin.y += NSMinY(thumb_start) - NSMinY(thumb_rect_);
    471   return rect;
    472 }
    473 
    474 // Changes |title_rect| and |favicon_rect| such that the favicon's and the
    475 // title's vertical center is aligned and that the minimum distance between
    476 // the thumb rect and favicon and title rects doesn't change.
    477 void Tile::set_font_metrics(CGFloat ascender, CGFloat descender) {
    478   // Make the title height big enough to fit the font, and adopt the title
    479   // position to keep its distance from the thumb rect.
    480   title_rect_.origin.y -= ascender + descender - NSHeight(title_rect_);
    481   title_rect_.size.height = ascender + descender;
    482 
    483   // Align vertical center. Both rects are currently aligned on their top edge.
    484   CGFloat delta_y = NSMidY(title_rect_) - NSMidY(favicon_rect_);
    485   if (delta_y > 0) {
    486     // Title is higher: Move favicon down to align the centers.
    487     favicon_rect_.origin.y += delta_y;
    488   } else {
    489     // Favicon is higher: Move title down to align the centers.
    490     title_rect_.origin.y -= delta_y;
    491   }
    492 }
    493 
    494 // A tileset is responsible for owning and laying out all |Tile|s shown in a
    495 // tabpose window.
    496 class TileSet {
    497  public:
    498   TileSet() {}
    499 
    500   // Fills in |tiles_|.
    501   void Build(TabStripModel* source_model);
    502 
    503   // Computes coordinates for |tiles_|.
    504   void Layout(NSRect containing_rect);
    505 
    506   int selected_index() const { return selected_index_; }
    507   void set_selected_index(int index);
    508 
    509   const Tile& selected_tile() const { return *tiles_[selected_index()]; }
    510   Tile& tile_at(int index) { return *tiles_[index]; }
    511   const Tile& tile_at(int index) const { return *tiles_[index]; }
    512 
    513   // These return which index needs to be selected when the user presses
    514   // up, down, left, or right respectively.
    515   int up_index() const;
    516   int down_index() const;
    517   int left_index() const;
    518   int right_index() const;
    519 
    520   // These return which index needs to be selected on tab / shift-tab.
    521   int next_index() const;
    522   int previous_index() const;
    523 
    524   // Inserts a new Tile object containing |contents| at |index|. Does no
    525   // relayout.
    526   void InsertTileAt(int index, content::WebContents* contents);
    527 
    528   // Removes the Tile object at |index|. Does no relayout.
    529   void RemoveTileAt(int index);
    530 
    531   // Moves the Tile object at |from_index| to |to_index|. Since this doesn't
    532   // change the number of tiles, relayout can be done just by swapping the
    533   // tile rectangles in the index interval [from_index, to_index], so this does
    534   // layout.
    535   void MoveTileFromTo(int from_index, int to_index);
    536 
    537  private:
    538   int count_x() const {
    539     return ceilf(tiles_.size() / static_cast<float>(count_y_));
    540   }
    541   int count_y() const {
    542     return count_y_;
    543   }
    544   int last_row_count_x() const {
    545     return tiles_.size() - count_x() * (count_y() - 1);
    546   }
    547   int tiles_in_row(int row) const {
    548     return row != count_y() - 1 ? count_x() : last_row_count_x();
    549   }
    550   void index_to_tile_xy(int index, int* tile_x, int* tile_y) const {
    551     *tile_x = index % count_x();
    552     *tile_y = index / count_x();
    553   }
    554   int tile_xy_to_index(int tile_x, int tile_y) const {
    555     return tile_y * count_x() + tile_x;
    556   }
    557 
    558   ScopedVector<Tile> tiles_;
    559   int selected_index_;
    560   int count_y_;
    561 
    562   DISALLOW_COPY_AND_ASSIGN(TileSet);
    563 };
    564 
    565 void TileSet::Build(TabStripModel* source_model) {
    566   selected_index_ =  source_model->active_index();
    567   tiles_.resize(source_model->count());
    568   for (size_t i = 0; i < tiles_.size(); ++i) {
    569     tiles_[i] = new Tile;
    570     tiles_[i]->contents_ = source_model->GetWebContentsAt(i);
    571   }
    572 }
    573 
    574 void TileSet::Layout(NSRect containing_rect) {
    575   int tile_count = tiles_.size();
    576   if (tile_count == 0)  // Happens e.g. during test shutdown.
    577     return;
    578 
    579   // Room around the tiles insde of |containing_rect|.
    580   const int kSmallPaddingTop = 30;
    581   const int kSmallPaddingLeft = 30;
    582   const int kSmallPaddingRight = 30;
    583   const int kSmallPaddingBottom = 30;
    584 
    585   // Favicon / title area.
    586   const int kThumbTitlePaddingY = 6;
    587   const int kFaviconSize = 16;
    588   const int kTitleHeight = 14;  // Font size.
    589   const int kTitleExtraHeight = kThumbTitlePaddingY + kTitleHeight;
    590   const int kFaviconExtraHeight = kThumbTitlePaddingY + kFaviconSize;
    591   const int kFaviconTitleDistanceX = 6;
    592   const int kFooterExtraHeight =
    593       std::max(kFaviconExtraHeight, kTitleExtraHeight);
    594 
    595   // Room between the tiles.
    596   const int kSmallPaddingX = 15;
    597   const int kSmallPaddingY = kFooterExtraHeight;
    598 
    599   // Aspect ratio of the containing rect.
    600   CGFloat aspect = NSWidth(containing_rect) / NSHeight(containing_rect);
    601 
    602   // Room left in container after the outer padding is removed.
    603   double container_width =
    604       NSWidth(containing_rect) - kSmallPaddingLeft - kSmallPaddingRight;
    605   double container_height =
    606       NSHeight(containing_rect) - kSmallPaddingTop - kSmallPaddingBottom;
    607 
    608   // The tricky part is figuring out the size of a tab thumbnail, or since the
    609   // size of the containing rect is known, the number of tiles in x and y
    610   // direction.
    611   // Given are the size of the containing rect, and the number of thumbnails
    612   // that need to fit into that rect. The aspect ratio of the thumbnails needs
    613   // to be the same as that of |containing_rect|, else they will look distorted.
    614   // The thumbnails need to be distributed such that
    615   // |count_x * count_y >= tile_count|, and such that wasted space is minimized.
    616   //  See the comments in
    617   // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding()| for a more
    618   // detailed discussion.
    619   // TODO(thakis): It might be good enough to choose |count_x| and |count_y|
    620   //   such that count_x / count_y is roughly equal to |aspect|?
    621   double fny = FitNRectsWithAspectIntoBoundingSizeWithConstantPadding(
    622       tile_count, aspect,
    623       container_width, container_height - kFooterExtraHeight,
    624       kSmallPaddingX, kSmallPaddingY + kFooterExtraHeight);
    625   count_y_ = roundf(fny);
    626 
    627   // Now that |count_x()| and |count_y_| are known, it's straightforward to
    628   // compute thumbnail width/height. See comment in
    629   // |FitNRectsWithAspectIntoBoundingSizeWithConstantPadding| for the derivation
    630   // of these two formulas.
    631   int small_width =
    632       floor((container_width + kSmallPaddingX) / static_cast<float>(count_x()) -
    633             kSmallPaddingX);
    634   int small_height =
    635       floor((container_height + kSmallPaddingY) / static_cast<float>(count_y_) -
    636             (kSmallPaddingY + kFooterExtraHeight));
    637 
    638   // |small_width / small_height| has only roughly an aspect ratio of |aspect|.
    639   // Shrink the thumbnail rect to make the aspect ratio fit exactly, and add
    640   // the extra space won by shrinking to the outer padding.
    641   int smallExtraPaddingLeft = 0;
    642   int smallExtraPaddingTop = 0;
    643   if (aspect > small_width/static_cast<float>(small_height)) {
    644     small_height = small_width / aspect;
    645     CGFloat all_tiles_height =
    646         (small_height + kSmallPaddingY + kFooterExtraHeight) * count_y() -
    647         (kSmallPaddingY + kFooterExtraHeight);
    648     smallExtraPaddingTop = (container_height - all_tiles_height)/2;
    649   } else {
    650     small_width = small_height * aspect;
    651     CGFloat all_tiles_width =
    652         (small_width + kSmallPaddingX) * count_x() - kSmallPaddingX;
    653     smallExtraPaddingLeft = (container_width - all_tiles_width)/2;
    654   }
    655 
    656   // Compute inter-tile padding in the zoomed-out view.
    657   CGFloat scale_small_to_big =
    658       NSWidth(containing_rect) / static_cast<float>(small_width);
    659   CGFloat big_padding_x = kSmallPaddingX * scale_small_to_big;
    660   CGFloat big_padding_y =
    661       (kSmallPaddingY + kFooterExtraHeight) * scale_small_to_big;
    662 
    663   // Now all dimensions are known. Lay out all tiles on a regular grid:
    664   // X X X X
    665   // X X X X
    666   // X X
    667   for (int row = 0, i = 0; i < tile_count; ++row) {
    668     for (int col = 0; col < count_x() && i < tile_count; ++col, ++i) {
    669       // Compute the smalled, zoomed-out thumbnail rect.
    670       tiles_[i]->thumb_rect_.size = NSMakeSize(small_width, small_height);
    671 
    672       int small_x = col * (small_width + kSmallPaddingX) +
    673                     kSmallPaddingLeft + smallExtraPaddingLeft;
    674       int small_y = row * (small_height + kSmallPaddingY + kFooterExtraHeight) +
    675                     kSmallPaddingTop + smallExtraPaddingTop;
    676 
    677       tiles_[i]->thumb_rect_.origin = NSMakePoint(
    678           small_x, NSHeight(containing_rect) - small_y - small_height);
    679 
    680       tiles_[i]->favicon_rect_.size = NSMakeSize(kFaviconSize, kFaviconSize);
    681       tiles_[i]->favicon_rect_.origin = NSMakePoint(
    682           small_x,
    683           NSHeight(containing_rect) -
    684               (small_y + small_height + kFaviconExtraHeight));
    685 
    686       // Align lower left corner of title rect with lower left corner of favicon
    687       // for now. The final position is computed later by
    688       // |Tile::set_font_metrics()|.
    689       tiles_[i]->title_font_size_ = kTitleHeight;
    690       tiles_[i]->title_rect_.origin = NSMakePoint(
    691           NSMaxX(tiles_[i]->favicon_rect()) + kFaviconTitleDistanceX,
    692           NSMinY(tiles_[i]->favicon_rect()));
    693       tiles_[i]->title_rect_.size = NSMakeSize(
    694           small_width -
    695               NSWidth(tiles_[i]->favicon_rect()) - kFaviconTitleDistanceX,
    696           kTitleHeight);
    697 
    698       // Compute the big, pre-zoom thumbnail rect.
    699       tiles_[i]->start_thumb_rect_.size = containing_rect.size;
    700 
    701       int big_x = col * (NSWidth(containing_rect) + big_padding_x);
    702       int big_y = row * (NSHeight(containing_rect) + big_padding_y);
    703       tiles_[i]->start_thumb_rect_.origin = NSMakePoint(big_x, -big_y);
    704     }
    705   }
    706 }
    707 
    708 void TileSet::set_selected_index(int index) {
    709   CHECK_GE(index, 0);
    710   CHECK_LT(index, static_cast<int>(tiles_.size()));
    711   selected_index_ = index;
    712 }
    713 
    714 // Given a |value| in [0, from_scale), map it into [0, to_scale) such that:
    715 // * [0, from_scale) ends up in the middle of [0, to_scale) if the latter is
    716 //   a bigger range
    717 // * The middle of [0, from_scale) is mapped to [0, to_scale), and the parts
    718 //   of the former that don't fit are mapped to 0 and to_scale - respectively
    719 //   if the former is a bigger range.
    720 static int rescale(int value, int from_scale, int to_scale) {
    721   int left = (to_scale - from_scale) / 2;
    722   int result = value + left;
    723   if (result < 0)
    724     return 0;
    725   if (result >= to_scale)
    726     return to_scale - 1;
    727   return result;
    728 }
    729 
    730 int TileSet::up_index() const {
    731   int tile_x, tile_y;
    732   index_to_tile_xy(selected_index(), &tile_x, &tile_y);
    733   tile_y -= 1;
    734   if (tile_y == count_y() - 2) {
    735     // Transition from last row to second-to-last row.
    736     tile_x = rescale(tile_x, last_row_count_x(), count_x());
    737   } else if (tile_y < 0) {
    738     // Transition from first row to last row.
    739     tile_x = rescale(tile_x, count_x(), last_row_count_x());
    740     tile_y = count_y() - 1;
    741   }
    742   return tile_xy_to_index(tile_x, tile_y);
    743 }
    744 
    745 int TileSet::down_index() const {
    746   int tile_x, tile_y;
    747   index_to_tile_xy(selected_index(), &tile_x, &tile_y);
    748   tile_y += 1;
    749   if (tile_y == count_y() - 1) {
    750     // Transition from second-to-last row to last row.
    751     tile_x = rescale(tile_x, count_x(), last_row_count_x());
    752   } else if (tile_y >= count_y()) {
    753     // Transition from last row to first row.
    754     tile_x = rescale(tile_x, last_row_count_x(), count_x());
    755     tile_y = 0;
    756   }
    757   return tile_xy_to_index(tile_x, tile_y);
    758 }
    759 
    760 int TileSet::left_index() const {
    761   int tile_x, tile_y;
    762   index_to_tile_xy(selected_index(), &tile_x, &tile_y);
    763   tile_x -= 1;
    764   if (tile_x < 0)
    765     tile_x = tiles_in_row(tile_y) - 1;
    766   return tile_xy_to_index(tile_x, tile_y);
    767 }
    768 
    769 int TileSet::right_index() const {
    770   int tile_x, tile_y;
    771   index_to_tile_xy(selected_index(), &tile_x, &tile_y);
    772   tile_x += 1;
    773   if (tile_x >= tiles_in_row(tile_y))
    774     tile_x = 0;
    775   return tile_xy_to_index(tile_x, tile_y);
    776 }
    777 
    778 int TileSet::next_index() const {
    779   int new_index = selected_index() + 1;
    780   if (new_index >= static_cast<int>(tiles_.size()))
    781     new_index = 0;
    782   return new_index;
    783 }
    784 
    785 int TileSet::previous_index() const {
    786   int new_index = selected_index() - 1;
    787   if (new_index < 0)
    788     new_index = tiles_.size() - 1;
    789   return new_index;
    790 }
    791 
    792 void TileSet::InsertTileAt(int index, content::WebContents* contents) {
    793   tiles_.insert(tiles_.begin() + index, new Tile);
    794   tiles_[index]->contents_ = contents;
    795 }
    796 
    797 void TileSet::RemoveTileAt(int index) {
    798   tiles_.erase(tiles_.begin() + index);
    799 }
    800 
    801 // Moves the Tile object at |from_index| to |to_index|. Also updates rectangles
    802 // so that the tiles stay in a left-to-right, top-to-bottom layout when walked
    803 // in sequential order.
    804 void TileSet::MoveTileFromTo(int from_index, int to_index) {
    805   NSRect thumb = tiles_[from_index]->thumb_rect_;
    806   NSRect start_thumb = tiles_[from_index]->start_thumb_rect_;
    807   NSRect favicon = tiles_[from_index]->favicon_rect_;
    808   NSRect title = tiles_[from_index]->title_rect_;
    809 
    810   scoped_ptr<Tile> tile(tiles_[from_index]);
    811   tiles_.weak_erase(tiles_.begin() + from_index);
    812   tiles_.insert(tiles_.begin() + to_index, tile.release());
    813 
    814   int step = from_index < to_index ? -1 : 1;
    815   for (int i = to_index; (i - from_index) * step < 0; i += step) {
    816     tiles_[i]->thumb_rect_ = tiles_[i + step]->thumb_rect_;
    817     tiles_[i]->start_thumb_rect_ = tiles_[i + step]->start_thumb_rect_;
    818     tiles_[i]->favicon_rect_ = tiles_[i + step]->favicon_rect_;
    819     tiles_[i]->title_rect_ = tiles_[i + step]->title_rect_;
    820   }
    821   tiles_[from_index]->thumb_rect_ = thumb;
    822   tiles_[from_index]->start_thumb_rect_ = start_thumb;
    823   tiles_[from_index]->favicon_rect_ = favicon;
    824   tiles_[from_index]->title_rect_ = title;
    825 }
    826 
    827 }  // namespace tabpose
    828 
    829 void AnimateScaledCALayerFrameFromTo(
    830     CALayer* layer,
    831     const NSRect& from, CGFloat from_scale,
    832     const NSRect& to, CGFloat to_scale,
    833     NSTimeInterval duration, id boundsAnimationDelegate) {
    834   // http://developer.apple.com/mac/library/qa/qa2008/qa1620.html
    835   CABasicAnimation* animation;
    836 
    837   animation = [CABasicAnimation animationWithKeyPath:@"bounds"];
    838   animation.fromValue = [NSValue valueWithRect:from];
    839   animation.toValue = [NSValue valueWithRect:to];
    840   animation.duration = duration;
    841   animation.timingFunction =
    842       [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    843   animation.delegate = boundsAnimationDelegate;
    844 
    845   // Update the layer's bounds so the layer doesn't snap back when the animation
    846   // completes.
    847   layer.bounds = NSRectToCGRect(to);
    848 
    849   // Add the animation, overriding the implicit animation.
    850   [layer addAnimation:animation forKey:@"bounds"];
    851 
    852   // Prepare the animation from the current position to the new position.
    853   NSPoint opoint = from.origin;
    854   NSPoint point = to.origin;
    855 
    856   // Adapt to anchorPoint.
    857   opoint.x += NSWidth(from) * from_scale * layer.anchorPoint.x;
    858   opoint.y += NSHeight(from) * from_scale * layer.anchorPoint.y;
    859   point.x += NSWidth(to) * to_scale * layer.anchorPoint.x;
    860   point.y += NSHeight(to) * to_scale * layer.anchorPoint.y;
    861 
    862   animation = [CABasicAnimation animationWithKeyPath:@"position"];
    863   animation.fromValue = [NSValue valueWithPoint:opoint];
    864   animation.toValue = [NSValue valueWithPoint:point];
    865   animation.duration = duration;
    866   animation.timingFunction =
    867       [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    868 
    869   // Update the layer's position so that the layer doesn't snap back when the
    870   // animation completes.
    871   layer.position = NSPointToCGPoint(point);
    872 
    873   // Add the animation, overriding the implicit animation.
    874   [layer addAnimation:animation forKey:@"position"];
    875 }
    876 
    877 void AnimateCALayerFrameFromTo(
    878     CALayer* layer, const NSRect& from, const NSRect& to,
    879     NSTimeInterval duration, id boundsAnimationDelegate) {
    880   AnimateScaledCALayerFrameFromTo(
    881       layer, from, 1.0, to, 1.0, duration, boundsAnimationDelegate);
    882 }
    883 
    884 void AnimateCALayerOpacityFromTo(
    885     CALayer* layer, double from, double to, NSTimeInterval duration) {
    886   CABasicAnimation* animation;
    887   animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
    888   animation.fromValue = [NSNumber numberWithFloat:from];
    889   animation.toValue = [NSNumber numberWithFloat:to];
    890   animation.duration = duration;
    891 
    892   layer.opacity = to;
    893   // Add the animation, overriding the implicit animation.
    894   [layer addAnimation:animation forKey:@"opacity"];
    895 }
    896 
    897 @interface TabposeWindow (Private)
    898 - (id)initForWindow:(NSWindow*)parent
    899                rect:(NSRect)rect
    900               slomo:(BOOL)slomo
    901       tabStripModel:(TabStripModel*)tabStripModel;
    902 
    903 // Creates and initializes the CALayer in the background and all the CALayers
    904 // for the thumbnails, favicons, and titles.
    905 - (void)setUpLayersInSlomo:(BOOL)slomo;
    906 
    907 // Tells the browser to make the tab corresponding to currently selected
    908 // thumbnail the current tab and starts the tabpose exit animmation.
    909 - (void)fadeAwayInSlomo:(BOOL)slomo;
    910 
    911 // Returns the CALayer for the close button belonging to the thumbnail at
    912 // index |index|.
    913 - (CALayer*)closebuttonLayerAtIndex:(NSUInteger)index;
    914 
    915 // Updates the visibility of all closebutton layers.
    916 - (void)updateClosebuttonLayersVisibility;
    917 @end
    918 
    919 @implementation TabposeWindow
    920 
    921 + (id)openTabposeFor:(NSWindow*)parent
    922                 rect:(NSRect)rect
    923                slomo:(BOOL)slomo
    924        tabStripModel:(TabStripModel*)tabStripModel {
    925   // Releases itself when closed.
    926   return [[TabposeWindow alloc]
    927       initForWindow:parent rect:rect slomo:slomo tabStripModel:tabStripModel];
    928 }
    929 
    930 - (id)initForWindow:(NSWindow*)parent
    931                rect:(NSRect)rect
    932               slomo:(BOOL)slomo
    933       tabStripModel:(TabStripModel*)tabStripModel {
    934   NSRect frame = [parent frame];
    935   if ((self = [super initWithContentRect:frame
    936                                styleMask:NSBorderlessWindowMask
    937                                  backing:NSBackingStoreBuffered
    938                                    defer:NO])) {
    939     containingRect_ = rect;
    940     tabStripModel_ = tabStripModel;
    941     state_ = tabpose::kFadingIn;
    942     tileSet_.reset(new tabpose::TileSet);
    943     tabStripModelObserverBridge_.reset(
    944         new TabStripModelObserverBridge(tabStripModel_, self));
    945     closeIcon_.reset([ResourceBundle::GetSharedInstance().GetNativeImageNamed(
    946             IDR_TABPOSE_CLOSE).ToNSImage() retain]);
    947     [self setReleasedWhenClosed:YES];
    948     [self setOpaque:NO];
    949     [self setBackgroundColor:[NSColor clearColor]];
    950     [self setUpLayersInSlomo:slomo];
    951     [self setAcceptsMouseMovedEvents:YES];
    952     [parent addChildWindow:self ordered:NSWindowAbove];
    953     [self makeKeyAndOrderFront:self];
    954   }
    955   return self;
    956 }
    957 
    958 - (CALayer*)selectedLayer {
    959   return [allThumbnailLayers_ objectAtIndex:tileSet_->selected_index()];
    960 }
    961 
    962 - (void)selectTileAtIndexWithoutAnimation:(int)newIndex {
    963   ScopedCAActionDisabler disabler;
    964   const tabpose::Tile& tile = tileSet_->tile_at(newIndex);
    965   selectionHighlight_.frame =
    966       NSRectToCGRect(NSInsetRect(tile.thumb_rect(),
    967                      -kSelectionInset, -kSelectionInset));
    968   tileSet_->set_selected_index(newIndex);
    969 
    970   [self updateClosebuttonLayersVisibility];
    971 }
    972 
    973 - (void)addLayersForTile:(tabpose::Tile&)tile
    974                 showZoom:(BOOL)showZoom
    975                    slomo:(BOOL)slomo
    976        animationDelegate:(id)animationDelegate {
    977   base::scoped_nsobject<CALayer> layer(
    978       [[ThumbnailLayer alloc] initWithWebContents:tile.web_contents()
    979                                          fullSize:tile.GetStartRectRelativeTo(
    980                                              tileSet_->selected_tile()).size]);
    981   [layer setNeedsDisplay];
    982 
    983   NSTimeInterval interval =
    984       kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
    985 
    986   // Background color as placeholder for now.
    987   layer.get().backgroundColor = CGColorGetConstantColor(kCGColorWhite);
    988   if (showZoom) {
    989     AnimateCALayerFrameFromTo(
    990         layer,
    991         tile.GetStartRectRelativeTo(tileSet_->selected_tile()),
    992         tile.thumb_rect(),
    993         interval,
    994         animationDelegate);
    995   } else {
    996     layer.get().frame = NSRectToCGRect(tile.thumb_rect());
    997   }
    998 
    999   layer.get().shadowRadius = 10;
   1000   layer.get().shadowOffset = CGSizeMake(0, -10);
   1001   if (state_ == tabpose::kFadedIn)
   1002     layer.get().shadowOpacity = 0.5;
   1003 
   1004   // Add a close button to the thumb layer.
   1005   CALayer* closeLayer = [CALayer layer];
   1006   closeLayer.contents = closeIcon_.get();
   1007   CGRect closeBounds = {};
   1008   closeBounds.size = NSSizeToCGSize([closeIcon_ size]);
   1009   closeLayer.bounds = closeBounds;
   1010   closeLayer.hidden = YES;
   1011 
   1012   [closeLayer addConstraint:
   1013       [CAConstraint constraintWithAttribute:kCAConstraintMidX
   1014                                  relativeTo:@"superlayer"
   1015                                   attribute:kCAConstraintMinX]];
   1016   [closeLayer addConstraint:
   1017       [CAConstraint constraintWithAttribute:kCAConstraintMidY
   1018                                  relativeTo:@"superlayer"
   1019                                   attribute:kCAConstraintMaxY]];
   1020 
   1021   layer.get().layoutManager = [CAConstraintLayoutManager layoutManager];
   1022   [layer.get() addSublayer:closeLayer];
   1023 
   1024   [bgLayer_ addSublayer:layer];
   1025   [allThumbnailLayers_ addObject:layer];
   1026 
   1027   // Favicon and title.
   1028   NSFont* font = [NSFont systemFontOfSize:tile.title_font_size()];
   1029   tile.set_font_metrics([font ascender], -[font descender]);
   1030 
   1031   CALayer* faviconLayer = [CALayer layer];
   1032   if (showZoom) {
   1033     AnimateCALayerFrameFromTo(
   1034         faviconLayer,
   1035         tile.GetFaviconStartRectRelativeTo(tileSet_->selected_tile()),
   1036         tile.favicon_rect(),
   1037         interval,
   1038         nil);
   1039     AnimateCALayerOpacityFromTo(faviconLayer, 0.0, 1.0, interval);
   1040   } else {
   1041     faviconLayer.frame = NSRectToCGRect(tile.favicon_rect());
   1042   }
   1043   faviconLayer.contents = tile.favicon();
   1044   faviconLayer.zPosition = 1;  // On top of the thumb shadow.
   1045   [bgLayer_ addSublayer:faviconLayer];
   1046   [allFaviconLayers_ addObject:faviconLayer];
   1047 
   1048   // CATextLayers can't animate their fontSize property, at least on 10.5.
   1049   // Animate transform.scale instead.
   1050 
   1051   // The scaling should have its origin in the layer's upper left corner.
   1052   // This needs to be set before |AnimateCALayerFrameFromTo()| is called.
   1053   CATextLayer* titleLayer = [CATextLayer layer];
   1054   titleLayer.anchorPoint = CGPointMake(0, 1);
   1055   if (showZoom) {
   1056     NSRect fromRect =
   1057         tile.GetTitleStartRectRelativeTo(tileSet_->selected_tile());
   1058     NSRect toRect = tile.title_rect();
   1059     CGFloat scale = NSWidth(fromRect) / NSWidth(toRect);
   1060     fromRect.size = toRect.size;
   1061 
   1062     // Add scale animation.
   1063     CABasicAnimation* scaleAnimation =
   1064         [CABasicAnimation animationWithKeyPath:@"transform.scale"];
   1065     scaleAnimation.fromValue = [NSNumber numberWithDouble:scale];
   1066     scaleAnimation.toValue = [NSNumber numberWithDouble:1.0];
   1067     scaleAnimation.duration = interval;
   1068     scaleAnimation.timingFunction =
   1069         [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
   1070     [titleLayer addAnimation:scaleAnimation forKey:@"transform.scale"];
   1071 
   1072     // Add the position and opacity animations.
   1073     AnimateScaledCALayerFrameFromTo(
   1074         titleLayer, fromRect, scale, toRect, 1.0, interval, nil);
   1075     AnimateCALayerOpacityFromTo(faviconLayer, 0.0, 1.0, interval);
   1076   } else {
   1077     titleLayer.frame = NSRectToCGRect(tile.title_rect());
   1078   }
   1079   titleLayer.string = base::SysUTF16ToNSString(tile.title());
   1080   titleLayer.fontSize = [font pointSize];
   1081   titleLayer.truncationMode = kCATruncationEnd;
   1082   titleLayer.font = font;
   1083   titleLayer.zPosition = 1;  // On top of the thumb shadow.
   1084   [bgLayer_ addSublayer:titleLayer];
   1085   [allTitleLayers_ addObject:titleLayer];
   1086 }
   1087 
   1088 - (void)setUpLayersInSlomo:(BOOL)slomo {
   1089   // Root layer -- covers whole window.
   1090   rootLayer_ = [CALayer layer];
   1091 
   1092   // In a block so that the layers don't fade in.
   1093   {
   1094     ScopedCAActionDisabler disabler;
   1095     // Background layer -- the visible part of the window.
   1096     gray_.reset(CGColorCreateGenericGray(kCentralGray, 1.0));
   1097     bgLayer_ = [CALayer layer];
   1098     bgLayer_.backgroundColor = gray_;
   1099     bgLayer_.frame = NSRectToCGRect(containingRect_);
   1100     bgLayer_.masksToBounds = YES;
   1101     [rootLayer_ addSublayer:bgLayer_];
   1102 
   1103     // Selection highlight layer.
   1104     darkBlue_.reset(CGColorCreateGenericRGB(0.25, 0.34, 0.86, 1.0));
   1105     selectionHighlight_ = [CALayer layer];
   1106     selectionHighlight_.backgroundColor = darkBlue_;
   1107     selectionHighlight_.cornerRadius = 5.0;
   1108     selectionHighlight_.zPosition = -1;  // Behind other layers.
   1109     selectionHighlight_.hidden = YES;
   1110     [bgLayer_ addSublayer:selectionHighlight_];
   1111 
   1112     // Bottom gradient.
   1113     CALayer* gradientLayer = [[[GrayGradientLayer alloc]
   1114         initWithStartGray:kCentralGray endGray:kBottomGray] autorelease];
   1115     gradientLayer.frame = CGRectMake(
   1116         0,
   1117         0,
   1118         NSWidth(containingRect_),
   1119         kBottomGradientHeight);
   1120     [gradientLayer setNeedsDisplay];  // Draw once.
   1121     [bgLayer_ addSublayer:gradientLayer];
   1122   }
   1123   // Top gradient (fades in).
   1124   CGFloat toolbarHeight = NSHeight([self frame]) - NSHeight(containingRect_);
   1125   topGradient_ = [[[GrayGradientLayer alloc]
   1126       initWithStartGray:kTopGray endGray:kCentralGray] autorelease];
   1127   topGradient_.frame = CGRectMake(
   1128       0,
   1129       NSHeight([self frame]) - toolbarHeight,
   1130       NSWidth(containingRect_),
   1131       toolbarHeight);
   1132   [topGradient_ setNeedsDisplay];  // Draw once.
   1133   [rootLayer_ addSublayer:topGradient_];
   1134   NSTimeInterval interval =
   1135       kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
   1136   AnimateCALayerOpacityFromTo(topGradient_, 0, 1, interval);
   1137 
   1138   // Layers for the tab thumbnails.
   1139   tileSet_->Build(tabStripModel_);
   1140   tileSet_->Layout(containingRect_);
   1141   allThumbnailLayers_.reset(
   1142       [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
   1143   allFaviconLayers_.reset(
   1144       [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
   1145   allTitleLayers_.reset(
   1146       [[NSMutableArray alloc] initWithCapacity:tabStripModel_->count()]);
   1147 
   1148   for (int i = 0; i < tabStripModel_->count(); ++i) {
   1149     // Add a delegate to one of the animations to get a notification once the
   1150     // animations are done.
   1151     [self  addLayersForTile:tileSet_->tile_at(i)
   1152                  showZoom:YES
   1153                     slomo:slomo
   1154         animationDelegate:i == tileSet_->selected_index() ? self : nil];
   1155     if (i == tileSet_->selected_index()) {
   1156       CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
   1157       CAAnimation* animation = [layer animationForKey:@"bounds"];
   1158       DCHECK(animation);
   1159       [animation setValue:kAnimationIdFadeIn forKey:kAnimationIdKey];
   1160     }
   1161   }
   1162   [self selectTileAtIndexWithoutAnimation:tileSet_->selected_index()];
   1163 
   1164   // Needs to happen after all layers have been added to |rootLayer_|, else
   1165   // there's a one frame flash of grey at the beginning of the animation
   1166   // (|bgLayer_| showing through with none of its children visible yet).
   1167   [[self contentView] setLayer:rootLayer_];
   1168   [[self contentView] setWantsLayer:YES];
   1169 }
   1170 
   1171 - (BOOL)canBecomeKeyWindow {
   1172  return YES;
   1173 }
   1174 
   1175 // Lets the traffic light buttons on the browser window keep their "active"
   1176 // state while an info bubble is open. Only has an effect on 10.7.
   1177 - (BOOL)_sharesParentKeyState {
   1178   return YES;
   1179 }
   1180 
   1181 // Handle key events that should be executed repeatedly while the key is down.
   1182 - (void)keyDown:(NSEvent*)event {
   1183   if (state_ == tabpose::kFadingOut)
   1184     return;
   1185   NSString* characters = [event characters];
   1186   if ([characters length] < 1)
   1187     return;
   1188 
   1189   unichar character = [characters characterAtIndex:0];
   1190   int newIndex = -1;
   1191   switch (character) {
   1192     case NSUpArrowFunctionKey:
   1193       newIndex = tileSet_->up_index();
   1194       break;
   1195     case NSDownArrowFunctionKey:
   1196       newIndex = tileSet_->down_index();
   1197       break;
   1198     case NSLeftArrowFunctionKey:
   1199       newIndex = tileSet_->left_index();
   1200       break;
   1201     case NSRightArrowFunctionKey:
   1202       newIndex = tileSet_->right_index();
   1203       break;
   1204     case NSTabCharacter:
   1205       newIndex = tileSet_->next_index();
   1206       break;
   1207     case NSBackTabCharacter:
   1208       newIndex = tileSet_->previous_index();
   1209       break;
   1210   }
   1211   if (newIndex != -1)
   1212     [self selectTileAtIndexWithoutAnimation:newIndex];
   1213 }
   1214 
   1215 // Handle keyboard events that should be executed once when the key is released.
   1216 - (void)keyUp:(NSEvent*)event {
   1217   if (state_ == tabpose::kFadingOut)
   1218     return;
   1219   NSString* characters = [event characters];
   1220   if ([characters length] < 1)
   1221     return;
   1222 
   1223   unichar character = [characters characterAtIndex:0];
   1224   switch (character) {
   1225     case NSEnterCharacter:
   1226     case NSNewlineCharacter:
   1227     case NSCarriageReturnCharacter:
   1228     case ' ':
   1229       [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1230       break;
   1231     case '\e':  // Escape
   1232       tileSet_->set_selected_index(tabStripModel_->active_index());
   1233       [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1234       break;
   1235   }
   1236 }
   1237 
   1238 // Handle keyboard events that contain cmd or ctrl.
   1239 - (BOOL)performKeyEquivalent:(NSEvent*)event {
   1240   if (state_ == tabpose::kFadingOut)
   1241     return NO;
   1242   NSString* characters = [event characters];
   1243   if ([characters length] < 1)
   1244     return NO;
   1245   unichar character = [characters characterAtIndex:0];
   1246   if ([event modifierFlags] & NSCommandKeyMask) {
   1247     if (character >= '1' && character <= '9') {
   1248       int index =
   1249           character == '9' ? tabStripModel_->count() - 1 : character - '1';
   1250       if (index < tabStripModel_->count()) {
   1251         tileSet_->set_selected_index(index);
   1252         [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1253         return YES;
   1254       }
   1255     }
   1256   }
   1257   return NO;
   1258 }
   1259 
   1260 - (void)flagsChanged:(NSEvent*)event {
   1261   showAllCloseLayers_ = ([event modifierFlags] & NSAlternateKeyMask) != 0;
   1262   [self updateClosebuttonLayersVisibility];
   1263 }
   1264 
   1265 - (void)selectTileFromMouseEvent:(NSEvent*)event {
   1266   int newIndex = -1;
   1267   CGPoint p = NSPointToCGPoint([event locationInWindow]);
   1268   for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
   1269     CALayer* layer = [allThumbnailLayers_ objectAtIndex:i];
   1270     CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
   1271     if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp])
   1272       newIndex = i;
   1273   }
   1274   if (newIndex >= 0)
   1275     [self selectTileAtIndexWithoutAnimation:newIndex];
   1276 }
   1277 
   1278 - (void)mouseMoved:(NSEvent*)event {
   1279   [self selectTileFromMouseEvent:event];
   1280 }
   1281 
   1282 - (CALayer*)closebuttonLayerAtIndex:(NSUInteger)index {
   1283   CALayer* layer = [allThumbnailLayers_ objectAtIndex:index];
   1284   return [[layer sublayers] objectAtIndex:0];
   1285 }
   1286 
   1287 - (void)updateClosebuttonLayersVisibility {
   1288   for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
   1289     CALayer* layer = [self closebuttonLayerAtIndex:i];
   1290     BOOL isSelectedTile = static_cast<int>(i) == tileSet_->selected_index();
   1291     BOOL isVisible = state_ == tabpose::kFadedIn &&
   1292                      (isSelectedTile || showAllCloseLayers_);
   1293     layer.hidden = !isVisible;
   1294   }
   1295 }
   1296 
   1297 - (void)mouseDown:(NSEvent*)event {
   1298   // Just in case the user clicked without ever moving the mouse.
   1299   [self selectTileFromMouseEvent:event];
   1300 
   1301   // If the click occurred in a close box, close that tab and don't do anything
   1302   // else.
   1303   CGPoint p = NSPointToCGPoint([event locationInWindow]);
   1304   for (NSUInteger i = 0; i < [allThumbnailLayers_ count]; ++i) {
   1305     CALayer* layer = [self closebuttonLayerAtIndex:i];
   1306     CGPoint lp = [layer convertPoint:p fromLayer:rootLayer_];
   1307     if ([static_cast<CALayer*>([layer presentationLayer]) containsPoint:lp] &&
   1308         !layer.hidden) {
   1309       tabStripModel_->CloseWebContentsAt(i,
   1310           TabStripModel::CLOSE_USER_GESTURE |
   1311           TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
   1312       return;
   1313     }
   1314   }
   1315 
   1316   [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1317 }
   1318 
   1319 - (void)swipeWithEvent:(NSEvent*)event {
   1320   if (abs([event deltaY]) > 0.5)  // Swipe up or down.
   1321     [self fadeAwayInSlomo:([event modifierFlags] & NSShiftKeyMask) != 0];
   1322 }
   1323 
   1324 - (void)close {
   1325   // Prevent parent window from disappearing.
   1326   [[self parentWindow] removeChildWindow:self];
   1327 
   1328   // We're dealloc'd in an autorelease pool by then the observer registry
   1329   // might be dead, so explicitly reset the observer now.
   1330   tabStripModelObserverBridge_.reset();
   1331 
   1332   [super close];
   1333 }
   1334 
   1335 - (void)commandDispatch:(id)sender {
   1336   if ([sender tag] == IDC_TABPOSE)
   1337     [self fadeAwayInSlomo:NO];
   1338 }
   1339 
   1340 - (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item {
   1341   // Disable all browser-related menu items except the tab overview toggle.
   1342   SEL action = [item action];
   1343   NSInteger tag = [item tag];
   1344   return action == @selector(commandDispatch:) && tag == IDC_TABPOSE;
   1345 }
   1346 
   1347 - (void)fadeAwayTileAtIndex:(int)index {
   1348   const tabpose::Tile& tile = tileSet_->tile_at(index);
   1349   CALayer* layer = [allThumbnailLayers_ objectAtIndex:index];
   1350   // Add a delegate to one of the implicit animations to get a notification
   1351   // once the animations are done.
   1352   if (static_cast<int>(index) == tileSet_->selected_index()) {
   1353     CAAnimation* animation = [CAAnimation animation];
   1354     animation.delegate = self;
   1355     [animation setValue:kAnimationIdFadeOut forKey:kAnimationIdKey];
   1356     [layer addAnimation:animation forKey:@"frame"];
   1357   }
   1358 
   1359   // Thumbnail.
   1360   layer.frame = NSRectToCGRect(
   1361       tile.GetStartRectRelativeTo(tileSet_->selected_tile()));
   1362 
   1363   if (static_cast<int>(index) == tileSet_->selected_index()) {
   1364     // Redraw layer at big resolution, so that zoom-in isn't blocky.
   1365     [layer setNeedsDisplay];
   1366   }
   1367 
   1368   // Title.
   1369   CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:index];
   1370   faviconLayer.frame = NSRectToCGRect(
   1371       tile.GetFaviconStartRectRelativeTo(tileSet_->selected_tile()));
   1372   faviconLayer.opacity = 0;
   1373 
   1374   // Favicon.
   1375   // The |fontSize| cannot be animated directly, animate the layer's scale
   1376   // instead. |transform.scale| affects the rendered width, so keep the small
   1377   // bounds.
   1378   CALayer* titleLayer = [allTitleLayers_ objectAtIndex:index];
   1379   NSRect titleRect = tile.title_rect();
   1380   NSRect titleToRect =
   1381       tile.GetTitleStartRectRelativeTo(tileSet_->selected_tile());
   1382   CGFloat scale = NSWidth(titleToRect) / NSWidth(titleRect);
   1383   titleToRect.origin.x +=
   1384       NSWidth(titleRect) * scale * titleLayer.anchorPoint.x;
   1385   titleToRect.origin.y +=
   1386       NSHeight(titleRect) * scale * titleLayer.anchorPoint.y;
   1387   titleLayer.position = NSPointToCGPoint(titleToRect.origin);
   1388   [titleLayer setValue:[NSNumber numberWithDouble:scale]
   1389             forKeyPath:@"transform.scale"];
   1390   titleLayer.opacity = 0;
   1391 }
   1392 
   1393 - (void)fadeAwayInSlomo:(BOOL)slomo {
   1394   if (state_ == tabpose::kFadingOut)
   1395     return;
   1396 
   1397   state_ = tabpose::kFadingOut;
   1398   [self setAcceptsMouseMovedEvents:NO];
   1399 
   1400   // Select chosen tab.
   1401   if (tileSet_->selected_index() < tabStripModel_->count()) {
   1402     tabStripModel_->ActivateTabAt(tileSet_->selected_index(),
   1403                                   /*user_gesture=*/true);
   1404   } else {
   1405     DCHECK_EQ(tileSet_->selected_index(), 0);
   1406   }
   1407 
   1408   {
   1409     ScopedCAActionDisabler disableCAActions;
   1410 
   1411     // Move the selected layer on top of all other layers.
   1412     [self selectedLayer].zPosition = 1;
   1413 
   1414     selectionHighlight_.hidden = YES;
   1415     // Running animations with shadows is slow, so turn shadows off before
   1416     // running the exit animation.
   1417     for (CALayer* layer in allThumbnailLayers_.get())
   1418       layer.shadowOpacity = 0.0;
   1419 
   1420     [self updateClosebuttonLayersVisibility];
   1421   }
   1422 
   1423   // Animate layers out, all in one transaction.
   1424   CGFloat duration =
   1425       1.3 * kDefaultAnimationDuration * (slomo ? kSlomoFactor : 1);
   1426   ScopedCAActionSetDuration durationSetter(duration);
   1427   for (int i = 0; i < tabStripModel_->count(); ++i)
   1428     [self fadeAwayTileAtIndex:i];
   1429   AnimateCALayerOpacityFromTo(topGradient_, 1, 0, duration);
   1430 }
   1431 
   1432 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)finished {
   1433   NSString* animationId = [animation valueForKey:kAnimationIdKey];
   1434   if ([animationId isEqualToString:kAnimationIdFadeIn]) {
   1435     if (finished && state_ == tabpose::kFadingIn) {
   1436       // If the user clicks while the fade in animation is still running,
   1437       // |state_| is already kFadingOut. In that case, don't do anything.
   1438       state_ = tabpose::kFadedIn;
   1439 
   1440       selectionHighlight_.hidden = NO;
   1441 
   1442       // Running animations with shadows is slow, so turn shadows on only after
   1443       // the animation is done.
   1444       ScopedCAActionDisabler disableCAActions;
   1445       for (CALayer* layer in allThumbnailLayers_.get())
   1446         layer.shadowOpacity = 0.5;
   1447 
   1448       [self updateClosebuttonLayersVisibility];
   1449     }
   1450   } else if ([animationId isEqualToString:kAnimationIdFadeOut]) {
   1451     DCHECK_EQ(tabpose::kFadingOut, state_);
   1452     [self close];
   1453   }
   1454 }
   1455 
   1456 - (NSUInteger)thumbnailLayerCount {
   1457   return [allThumbnailLayers_ count];
   1458 }
   1459 
   1460 - (int)selectedIndex {
   1461   return tileSet_->selected_index();
   1462 }
   1463 
   1464 #pragma mark TabStripModelBridge
   1465 
   1466 - (void)refreshLayerFramesAtIndex:(int)i {
   1467   const tabpose::Tile& tile = tileSet_->tile_at(i);
   1468 
   1469   CALayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:i];
   1470 
   1471   if (i == tileSet_->selected_index()) {
   1472     AnimateCALayerFrameFromTo(
   1473         selectionHighlight_,
   1474         NSInsetRect(NSRectFromCGRect(thumbLayer.frame),
   1475                     -kSelectionInset, -kSelectionInset),
   1476         NSInsetRect(tile.thumb_rect(),
   1477                     -kSelectionInset, -kSelectionInset),
   1478         kObserverChangeAnimationDuration,
   1479         nil);
   1480   }
   1481 
   1482   // Repaint layer if necessary.
   1483   if (!NSEqualSizes(NSRectFromCGRect(thumbLayer.frame).size,
   1484                     tile.thumb_rect().size)) {
   1485     [thumbLayer setNeedsDisplay];
   1486   }
   1487 
   1488   // Use AnimateCALayerFrameFromTo() instead of just setting |frame| to let
   1489   // the animation match the selection animation --
   1490   // |kCAMediaTimingFunctionDefault| is 10.6-only.
   1491   AnimateCALayerFrameFromTo(
   1492       thumbLayer,
   1493       NSRectFromCGRect(thumbLayer.frame),
   1494       tile.thumb_rect(),
   1495       kObserverChangeAnimationDuration,
   1496       nil);
   1497 
   1498   CALayer* faviconLayer = [allFaviconLayers_ objectAtIndex:i];
   1499   AnimateCALayerFrameFromTo(
   1500       faviconLayer,
   1501       NSRectFromCGRect(faviconLayer.frame),
   1502       tile.favicon_rect(),
   1503       kObserverChangeAnimationDuration,
   1504       nil);
   1505 
   1506   CALayer* titleLayer = [allTitleLayers_ objectAtIndex:i];
   1507   AnimateCALayerFrameFromTo(
   1508       titleLayer,
   1509       NSRectFromCGRect(titleLayer.frame),
   1510       tile.title_rect(),
   1511       kObserverChangeAnimationDuration,
   1512       nil);
   1513 }
   1514 
   1515 - (void)insertTabWithContents:(content::WebContents*)contents
   1516                       atIndex:(NSInteger)index
   1517                  inForeground:(bool)inForeground {
   1518   // This happens if you cmd-click a link and then immediately open tabpose
   1519   // on a slowish machine.
   1520   ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
   1521 
   1522   // Insert new layer and relayout.
   1523   tileSet_->InsertTileAt(index, contents);
   1524   tileSet_->Layout(containingRect_);
   1525   [self  addLayersForTile:tileSet_->tile_at(index)
   1526                  showZoom:NO
   1527                     slomo:NO
   1528         animationDelegate:nil];
   1529 
   1530   // Update old layers.
   1531   DCHECK_EQ(tabStripModel_->count(),
   1532             static_cast<int>([allThumbnailLayers_ count]));
   1533   DCHECK_EQ(tabStripModel_->count(),
   1534             static_cast<int>([allTitleLayers_ count]));
   1535   DCHECK_EQ(tabStripModel_->count(),
   1536             static_cast<int>([allFaviconLayers_ count]));
   1537 
   1538   // Update selection.
   1539   int selectedIndex = tileSet_->selected_index();
   1540   if (selectedIndex >= index)
   1541     selectedIndex++;
   1542   [self selectTileAtIndexWithoutAnimation:selectedIndex];
   1543 
   1544   // Animate everything into its new place.
   1545   for (int i = 0; i < tabStripModel_->count(); ++i) {
   1546     if (i == index)  // The new layer.
   1547       continue;
   1548     [self refreshLayerFramesAtIndex:i];
   1549   }
   1550 }
   1551 
   1552 - (void)tabClosingWithContents:(content::WebContents*)contents
   1553                        atIndex:(NSInteger)index {
   1554   // We will also get a -tabDetachedWithContents:atIndex: notification for
   1555   // closing tabs, so do nothing here.
   1556 }
   1557 
   1558 - (void)tabDetachedWithContents:(content::WebContents*)contents
   1559                         atIndex:(NSInteger)index {
   1560   ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
   1561 
   1562   // Remove layer and relayout.
   1563   tileSet_->RemoveTileAt(index);
   1564   tileSet_->Layout(containingRect_);
   1565 
   1566   {
   1567     ScopedCAActionDisabler disabler;
   1568     [[allThumbnailLayers_ objectAtIndex:index] removeFromSuperlayer];
   1569     [allThumbnailLayers_ removeObjectAtIndex:index];
   1570     [[allTitleLayers_ objectAtIndex:index] removeFromSuperlayer];
   1571     [allTitleLayers_ removeObjectAtIndex:index];
   1572     [[allFaviconLayers_ objectAtIndex:index] removeFromSuperlayer];
   1573     [allFaviconLayers_ removeObjectAtIndex:index];
   1574   }
   1575 
   1576   // Update old layers.
   1577   DCHECK_EQ(tabStripModel_->count(),
   1578             static_cast<int>([allThumbnailLayers_ count]));
   1579   DCHECK_EQ(tabStripModel_->count(),
   1580             static_cast<int>([allTitleLayers_ count]));
   1581   DCHECK_EQ(tabStripModel_->count(),
   1582             static_cast<int>([allFaviconLayers_ count]));
   1583 
   1584   if (tabStripModel_->count() == 0)
   1585     [self close];
   1586 
   1587   // Update selection.
   1588   int selectedIndex = tileSet_->selected_index();
   1589   if (selectedIndex > index || selectedIndex >= tabStripModel_->count())
   1590     selectedIndex--;
   1591   if (selectedIndex >= 0)
   1592     [self selectTileAtIndexWithoutAnimation:selectedIndex];
   1593 
   1594   // Animate everything into its new place.
   1595   for (int i = 0; i < tabStripModel_->count(); ++i)
   1596     [self refreshLayerFramesAtIndex:i];
   1597 }
   1598 
   1599 - (void)tabMovedWithContents:(content::WebContents*)contents
   1600                     fromIndex:(NSInteger)from
   1601                       toIndex:(NSInteger)to {
   1602   ScopedCAActionSetDuration durationSetter(kObserverChangeAnimationDuration);
   1603 
   1604   // Move tile from |from| to |to|.
   1605   tileSet_->MoveTileFromTo(from, to);
   1606 
   1607   // Move corresponding layers from |from| to |to|.
   1608   base::scoped_nsobject<CALayer> thumbLayer(
   1609       [[allThumbnailLayers_ objectAtIndex:from] retain]);
   1610   [allThumbnailLayers_ removeObjectAtIndex:from];
   1611   [allThumbnailLayers_ insertObject:thumbLayer.get() atIndex:to];
   1612   base::scoped_nsobject<CALayer> faviconLayer(
   1613       [[allFaviconLayers_ objectAtIndex:from] retain]);
   1614   [allFaviconLayers_ removeObjectAtIndex:from];
   1615   [allFaviconLayers_ insertObject:faviconLayer.get() atIndex:to];
   1616   base::scoped_nsobject<CALayer> titleLayer(
   1617       [[allTitleLayers_ objectAtIndex:from] retain]);
   1618   [allTitleLayers_ removeObjectAtIndex:from];
   1619   [allTitleLayers_ insertObject:titleLayer.get() atIndex:to];
   1620 
   1621   // Update selection.
   1622   int selectedIndex = tileSet_->selected_index();
   1623   if (from == selectedIndex)
   1624     selectedIndex = to;
   1625   else if (from < selectedIndex && selectedIndex <= to)
   1626     selectedIndex--;
   1627   else if (to <= selectedIndex && selectedIndex < from)
   1628     selectedIndex++;
   1629   [self selectTileAtIndexWithoutAnimation:selectedIndex];
   1630 
   1631   // Update frames of the layers.
   1632   for (int i = std::min(from, to); i <= std::max(from, to); ++i)
   1633     [self refreshLayerFramesAtIndex:i];
   1634 }
   1635 
   1636 - (void)tabChangedWithContents:(content::WebContents*)contents
   1637                        atIndex:(NSInteger)index
   1638                     changeType:(TabStripModelObserver::TabChangeType)change {
   1639   // Tell the window to update text, title, and thumb layers at |index| to get
   1640   // their data from |contents|. |contents| can be different from the old
   1641   // contents at that index!
   1642   // While a tab is loading, this is unfortunately called quite often for
   1643   // both the "loading" and the "all" change types, so we don't really want to
   1644   // send thumb requests to the corresponding renderer when this is called.
   1645   // For now, just make sure that we don't hold on to an invalid WebContents
   1646   // object.
   1647   tabpose::Tile& tile = tileSet_->tile_at(index);
   1648   if (contents == tile.web_contents()) {
   1649     // TODO(thakis): Install a timer to send a thumb request/update title/update
   1650     // favicon after 20ms or so, and reset the timer every time this is called
   1651     // to make sure we get an updated thumb, without requesting them all over.
   1652     return;
   1653   }
   1654 
   1655   tile.set_tab_contents(contents);
   1656   ThumbnailLayer* thumbLayer = [allThumbnailLayers_ objectAtIndex:index];
   1657   [thumbLayer setWebContents:contents];
   1658 }
   1659 
   1660 - (void)tabStripModelDeleted {
   1661   [self close];
   1662 }
   1663 
   1664 @end
   1665