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