Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / browser_window_fullscreen_transition.mm
blobe0ac0bc88d7aa6254f2535d24186fdf02d481c20
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/browser_window_fullscreen_transition.h"
7 #include <QuartzCore/QuartzCore.h>
9 #include "base/mac/bind_objc_block.h"
10 #include "base/mac/foundation_util.h"
11 #include "base/mac/mac_util.h"
12 #import "base/mac/sdk_forward_declarations.h"
13 #include "base/memory/scoped_ptr.h"
14 #import "chrome/browser/ui/cocoa/framed_browser_window.h"
16 namespace {
18 NSString* const kPrimaryWindowAnimationID = @"PrimaryWindowAnimationID";
19 NSString* const kSnapshotWindowAnimationID = @"SnapshotWindowAnimationID";
20 NSString* const kAnimationIDKey = @"AnimationIDKey";
22 // This class has two simultaneous animations to resize and reposition layers.
23 // These animations must use the same timing function, otherwise there will be
24 // visual discordance.
25 NSString* TransformAnimationTimingFunction() {
26   return kCAMediaTimingFunctionEaseInEaseOut;
29 // This class locks and unlocks the FrameBrowserWindow. Its destructor ensures
30 // that the lock gets released.
31 class FrameAndStyleLock {
32  public:
33   explicit FrameAndStyleLock(FramedBrowserWindow* window) : window_(window) {}
35   ~FrameAndStyleLock() { set_lock(NO); }
37   void set_lock(bool lock) { [window_ setFrameAndStyleMaskLock:lock]; }
39  private:
40   FramedBrowserWindow* window_;  // weak
42   DISALLOW_COPY_AND_ASSIGN(FrameAndStyleLock);
45 }  // namespace
47 @interface BrowserWindowFullscreenTransition () {
48   // Flag to keep track of whether we are entering or exiting fullscreen.
49   BOOL isEnteringFullscreen_;
51   // The window which is undergoing the fullscreen transition.
52   base::scoped_nsobject<FramedBrowserWindow> primaryWindow_;
54   // A layer that holds a snapshot of the original state of |primaryWindow_|.
55   base::scoped_nsobject<CALayer> snapshotLayer_;
57   // A temporary window that holds |snapshotLayer_|.
58   base::scoped_nsobject<NSWindow> snapshotWindow_;
60   // The background color of |primaryWindow_| before the transition began.
61   base::scoped_nsobject<NSColor> primaryWindowInitialBackgroundColor_;
63   // Whether |primaryWindow_| was opaque before the transition began.
64   BOOL primaryWindowInitialOpaque_;
66   // Whether the instance is in the process of changing the size of
67   // |primaryWindow_|.
68   BOOL changingPrimaryWindowSize_;
70   // The frame of the |primaryWindow_| before it starts the transition.
71   NSRect initialFrame_;
73   // The frame that |primaryWindow_| is expected to have after the transition
74   // is finished.
75   NSRect finalFrame_;
77   // Locks and unlocks the FullSizeContentWindow.
78   scoped_ptr<FrameAndStyleLock> lock_;
81 // Takes a snapshot of |primaryWindow_| and puts it in |snapshotLayer_|.
82 - (void)takeSnapshot;
84 // Creates |snapshotWindow_| and adds |snapshotLayer_| to it.
85 - (void)makeAndPrepareSnapshotWindow;
87 // This method has several effects on |primaryWindow_|:
88 //  - Saves current state.
89 //  - Makes window transparent, with clear background.
90 //  - If we are entering fullscreen, it will also:
91 //    - Add NSFullScreenWindowMask style mask.
92 //    - Set the size to the screen's size.
93 - (void)preparePrimaryWindowForAnimation;
95 // Applies the fullscreen animation to |snapshotLayer_|.
96 - (void)animateSnapshotWindowWithDuration:(CGFloat)duration;
98 // Sets |primaryWindow_|'s frame to the expected frame.
99 - (void)changePrimaryWindowToFinalFrame;
101 // Override of CAAnimation delegate method.
102 - (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)flag;
104 // Returns the layer of the root view of |window|.
105 - (CALayer*)rootLayerOfWindow:(NSWindow*)window;
107 // Convert the point to be relative to the screen the primary window is on.
108 // This is important because if we're using multiple screens, the coordinate
109 // system extends to the second screen.
111 // For example, if the screen width is 1440, the second screen's frame origin
112 // is located at (1440, 0) and any x coordinate on the second screen will be
113 // >= 1440. If we move a window on the first screen to the same location on
114 // second screen, the window's frame origin will change from (x, y) to
115 // (x + 1440, y).
117 // When we animate the window, we want to use (x, y), the coordinates that are
118 // relative to the second screen. As a result, we use this method to convert a
119 // NSPoint so that it's relative to the screen it's on.
120 - (NSPoint)pointRelativeToCurrentScreen:(NSPoint)point;
122 @end
124 @implementation BrowserWindowFullscreenTransition
126 // -------------------------Public Methods----------------------------
128 - (instancetype)initEnterWithWindow:(FramedBrowserWindow*)window {
129   DCHECK(window);
130   DCHECK([self rootLayerOfWindow:window]);
131   if ((self = [super init])) {
132     primaryWindow_.reset([window retain]);
134     isEnteringFullscreen_ = YES;
135     initialFrame_ = [primaryWindow_ frame];
136     finalFrame_ = [[primaryWindow_ screen] frame];
137   }
138   return self;
141 - (instancetype)initExitWithWindow:(FramedBrowserWindow*)window
142                              frame:(NSRect)frame {
143   DCHECK(window);
144   DCHECK([self rootLayerOfWindow:window]);
145   if ((self = [super init])) {
146     primaryWindow_.reset([window retain]);
148     isEnteringFullscreen_ = NO;
149     finalFrame_ = frame;
150     initialFrame_ = [[primaryWindow_ screen] frame];
152     lock_.reset(new FrameAndStyleLock(window));
153   }
154   return self;
157 - (NSArray*)customWindowsForFullScreenTransition {
158   [self takeSnapshot];
159   [self makeAndPrepareSnapshotWindow];
160   return @[ primaryWindow_.get(), snapshotWindow_.get() ];
163 - (void)startCustomFullScreenAnimationWithDuration:(NSTimeInterval)duration {
164   [self preparePrimaryWindowForAnimation];
165   [self animatePrimaryWindowWithDuration:duration];
166   [self animateSnapshotWindowWithDuration:duration];
169 - (BOOL)shouldWindowBeUnconstrained {
170   return changingPrimaryWindowSize_;
173 - (NSSize)desiredWindowLayoutSize {
174   return isEnteringFullscreen_ ? [primaryWindow_ frame].size
175                                : [[primaryWindow_ contentView] bounds].size;
178 // -------------------------Private Methods----------------------------
180 - (void)takeSnapshot {
181   base::ScopedCFTypeRef<CGImageRef> windowSnapshot(CGWindowListCreateImage(
182       CGRectNull, kCGWindowListOptionIncludingWindow,
183       [primaryWindow_ windowNumber], kCGWindowImageBoundsIgnoreFraming));
184   snapshotLayer_.reset([[CALayer alloc] init]);
185   [snapshotLayer_ setFrame:NSRectToCGRect([primaryWindow_ frame])];
186   [snapshotLayer_ setContents:static_cast<id>(windowSnapshot.get())];
187   [snapshotLayer_ setAnchorPoint:CGPointMake(0, 0)];
188   CGColorRef colorRef = CGColorCreateGenericRGB(0, 0, 0, 0);
189   [snapshotLayer_ setBackgroundColor:colorRef];
190   CGColorRelease(colorRef);
193 - (void)makeAndPrepareSnapshotWindow {
194   DCHECK(snapshotLayer_);
196   snapshotWindow_.reset([[NSWindow alloc]
197       initWithContentRect:[[primaryWindow_ screen] frame]
198                 styleMask:0
199                   backing:NSBackingStoreBuffered
200                     defer:NO]);
201   [[snapshotWindow_ contentView] setWantsLayer:YES];
202   [snapshotWindow_ setOpaque:NO];
203   [snapshotWindow_ setBackgroundColor:[NSColor clearColor]];
204   [snapshotWindow_ setAnimationBehavior:NSWindowAnimationBehaviorNone];
206   [[[snapshotWindow_ contentView] layer] addSublayer:snapshotLayer_];
208   // Compute the frame of the snapshot layer such that the snapshot is
209   // positioned exactly on top of the original position of |primaryWindow_|.
210   NSRect snapshotLayerFrame =
211       [snapshotWindow_ convertRectFromScreen:[primaryWindow_ frame]];
212   [snapshotLayer_ setFrame:snapshotLayerFrame];
215 - (void)preparePrimaryWindowForAnimation {
216   // Save the initial state of the primary window.
217   primaryWindowInitialBackgroundColor_.reset(
218       [[primaryWindow_ backgroundColor] copy]);
219   primaryWindowInitialOpaque_ = [primaryWindow_ isOpaque];
221   // Make |primaryWindow_| invisible. This must happen before the window is
222   // resized, since resizing the window will call drawRect: and cause content
223   // to flash over the entire screen.
224   [primaryWindow_ setOpaque:NO];
225   CALayer* root = [self rootLayerOfWindow:primaryWindow_];
226   root.opacity = 0;
228   if (isEnteringFullscreen_) {
229     // As soon as the style mask includes the flag NSFullScreenWindowMask, the
230     // window is expected to receive fullscreen layout. This must be set before
231     // the window is resized, as that causes a relayout.
232     [primaryWindow_
233         setStyleMask:[primaryWindow_ styleMask] | NSFullScreenWindowMask];
234     [self changePrimaryWindowToFinalFrame];
235   } else {
236     // Set the size of the root layer to the size of the final frame. The root
237     // layer is placed at position (0, 0) because the animation will take care
238     // of the layer's start and end position.
239     root.frame =
240         NSMakeRect(0, 0, finalFrame_.size.width, finalFrame_.size.height);
242     // Right before the animation begins, change the contentView size to the
243     // expected size at the end of the animation. Afterwards, lock the
244     // |primaryWindow_| so that AppKit will not be able to make unwanted
245     // changes to it during the animation.
246     [primaryWindow_ forceContentViewSize:finalFrame_.size];
247     lock_->set_lock(YES);
248   }
251 - (void)animateSnapshotWindowWithDuration:(CGFloat)duration {
252   [snapshotWindow_ orderFront:nil];
254   // Calculate the frame so that it's relative to the screen.
255   NSRect finalFrameRelativeToScreen =
256       [snapshotWindow_ convertRectFromScreen:finalFrame_];
258   // Move the snapshot layer until it's bottom-left corner is at the the
259   // bottom-left corner of the expected frame.
260   CABasicAnimation* positionAnimation =
261       [CABasicAnimation animationWithKeyPath:@"position"];
262   positionAnimation.toValue =
263       [NSValue valueWithPoint:finalFrameRelativeToScreen.origin];
264   positionAnimation.timingFunction = [CAMediaTimingFunction
265       functionWithName:TransformAnimationTimingFunction()];
267   // Resize the bounds until it reaches the expected size at the end of the
268   // animation.
269   NSRect finalBounds =
270       NSMakeRect(0, 0, NSWidth(finalFrame_), NSHeight(finalFrame_));
271   CABasicAnimation* boundsAnimation =
272       [CABasicAnimation animationWithKeyPath:@"bounds"];
273   boundsAnimation.toValue = [NSValue valueWithRect:finalBounds];
274   boundsAnimation.timingFunction = [CAMediaTimingFunction
275       functionWithName:TransformAnimationTimingFunction()];
277   // Fade out the snapshot layer.
278   CABasicAnimation* opacityAnimation =
279       [CABasicAnimation animationWithKeyPath:@"opacity"];
280   opacityAnimation.toValue = @(0.0);
281   opacityAnimation.timingFunction =
282       [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
284   // Fill forwards, and don't remove the animation. When the animation
285   // completes, the entire window will be removed.
286   CAAnimationGroup* group = [CAAnimationGroup animation];
287   group.removedOnCompletion = NO;
288   group.fillMode = kCAFillModeForwards;
289   group.animations = @[ positionAnimation, boundsAnimation, opacityAnimation ];
290   group.duration = duration;
291   [group setValue:kSnapshotWindowAnimationID forKey:kAnimationIDKey];
292   group.delegate = self;
294   [snapshotLayer_ addAnimation:group forKey:nil];
297 - (void)animatePrimaryWindowWithDuration:(CGFloat)duration {
298   // As soon as the window's root layer is scaled down, the opacity should be
299   // set back to 1. There are a couple of ways to do this. The easiest is to
300   // just have a dummy animation as part of the same animation group.
301   CABasicAnimation* opacityAnimation =
302       [CABasicAnimation animationWithKeyPath:@"opacity"];
303   opacityAnimation.fromValue = @(1.0);
304   opacityAnimation.toValue = @(1.0);
306   // The root layer's size should start scaled down to the initial size of
307   // |primaryWindow_|. The animation increases the size until the root layer
308   // fills the screen.
309   NSRect initialFrame = initialFrame_;
310   NSRect endFrame = finalFrame_;
311   CGFloat xScale = NSWidth(initialFrame) / NSWidth(endFrame);
312   CGFloat yScale = NSHeight(initialFrame) / NSHeight(endFrame);
313   CATransform3D initial = CATransform3DMakeScale(xScale, yScale, 1);
314   CABasicAnimation* transformAnimation =
315       [CABasicAnimation animationWithKeyPath:@"transform"];
316   transformAnimation.fromValue = [NSValue valueWithCATransform3D:initial];
318   // Animate the primary window from its initial position, the center of the
319   // initial window.
320   CABasicAnimation* positionAnimation =
321       [CABasicAnimation animationWithKeyPath:@"position"];
322   NSPoint centerOfInitialFrame =
323       NSMakePoint(NSMidX(initialFrame), NSMidY(initialFrame));
324   NSPoint startingLayerPoint =
325       [self pointRelativeToCurrentScreen:centerOfInitialFrame];
326   positionAnimation.fromValue = [NSValue valueWithPoint:startingLayerPoint];
328   // Since the root layer's frame is different from the window, AppKit might
329   // animate it to a different position if we have multiple windows in
330   // fullscreen. This ensures that the animation moves to the correct position.
331   CGFloat anchorPointX = NSWidth(endFrame) / 2;
332   CGFloat anchorPointY = NSHeight(endFrame) / 2;
333   NSPoint endLayerPoint = [self pointRelativeToCurrentScreen:endFrame.origin];
334   positionAnimation.toValue =
335       [NSValue valueWithPoint:NSMakePoint(endLayerPoint.x + anchorPointX,
336                                           endLayerPoint.y + anchorPointY)];
338   CAAnimationGroup* group = [CAAnimationGroup animation];
339   group.removedOnCompletion = NO;
340   group.fillMode = kCAFillModeForwards;
341   group.animations =
342       @[ opacityAnimation, positionAnimation, transformAnimation ];
343   group.timingFunction = [CAMediaTimingFunction
344       functionWithName:TransformAnimationTimingFunction()];
345   group.duration = duration;
346   [group setValue:kPrimaryWindowAnimationID forKey:kAnimationIDKey];
347   group.delegate = self;
349   CALayer* root = [self rootLayerOfWindow:primaryWindow_];
350   [root addAnimation:group forKey:kPrimaryWindowAnimationID];
353 - (void)changePrimaryWindowToFinalFrame {
354   changingPrimaryWindowSize_ = YES;
355   [primaryWindow_ setFrame:finalFrame_ display:NO];
356   changingPrimaryWindowSize_ = NO;
359 - (void)animationDidStop:(CAAnimation*)theAnimation finished:(BOOL)flag {
360   NSString* animationID = [theAnimation valueForKey:kAnimationIDKey];
362   // Remove the snapshot window.
363   if ([animationID isEqual:kSnapshotWindowAnimationID]) {
364     [snapshotWindow_ orderOut:nil];
365     snapshotWindow_.reset();
366     snapshotLayer_.reset();
367     return;
368   }
370   if ([animationID isEqual:kPrimaryWindowAnimationID]) {
371     // If we're exiting full screen, we want to set the |primaryWindow_|'s
372     // frame to the expected frame at the end of the animation. The window's
373     // lock must also be released.
374     if (!isEnteringFullscreen_) {
375       lock_->set_lock(NO);
376       [primaryWindow_
377           setStyleMask:[primaryWindow_ styleMask] & ~NSFullScreenWindowMask];
378       [self changePrimaryWindowToFinalFrame];
379     }
381     // Checks if the contentView size is correct.
382     NSSize expectedSize = finalFrame_.size;
383     NSView* content = [primaryWindow_ contentView];
384     DCHECK_EQ(NSHeight(content.frame), expectedSize.height);
385     DCHECK_EQ(NSWidth(content.frame), expectedSize.width);
387     // Restore the state of the primary window and make it visible again.
388     [primaryWindow_ setOpaque:primaryWindowInitialOpaque_];
389     [primaryWindow_ setBackgroundColor:primaryWindowInitialBackgroundColor_];
391     CALayer* root = [self rootLayerOfWindow:primaryWindow_];
392     [root removeAnimationForKey:kPrimaryWindowAnimationID];
393     root.opacity = 1;
394   }
397 - (CALayer*)rootLayerOfWindow:(NSWindow*)window {
398   return [[[window contentView] superview] layer];
401 - (NSPoint)pointRelativeToCurrentScreen:(NSPoint)point {
402   NSRect screenFrame = [[primaryWindow_ screen] frame];
403   return NSMakePoint(point.x - screenFrame.origin.x,
404                      point.y - screenFrame.origin.y);
407 @end