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"
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 {
33 explicit FrameAndStyleLock(FramedBrowserWindow* window) : window_(window) {}
35 ~FrameAndStyleLock() { set_lock(NO); }
37 void set_lock(bool lock) { [window_ setFrameAndStyleMaskLock:lock]; }
40 FramedBrowserWindow* window_; // weak
42 DISALLOW_COPY_AND_ASSIGN(FrameAndStyleLock);
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
68 BOOL changingPrimaryWindowSize_;
70 // The frame of the |primaryWindow_| before it starts the transition.
73 // The frame that |primaryWindow_| is expected to have after the transition
77 // Locks and unlocks the FullSizeContentWindow.
78 scoped_ptr<FrameAndStyleLock> lock_;
81 // Takes a snapshot of |primaryWindow_| and puts it in |snapshotLayer_|.
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
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;
124 @implementation BrowserWindowFullscreenTransition
126 // -------------------------Public Methods----------------------------
128 - (instancetype)initEnterWithWindow:(FramedBrowserWindow*)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];
141 - (instancetype)initExitWithWindow:(FramedBrowserWindow*)window
142 frame:(NSRect)frame {
144 DCHECK([self rootLayerOfWindow:window]);
145 if ((self = [super init])) {
146 primaryWindow_.reset([window retain]);
148 isEnteringFullscreen_ = NO;
150 initialFrame_ = [[primaryWindow_ screen] frame];
152 lock_.reset(new FrameAndStyleLock(window));
157 - (NSArray*)customWindowsForFullScreenTransition {
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]
199 backing:NSBackingStoreBuffered
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_];
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.
233 setStyleMask:[primaryWindow_ styleMask] | NSFullScreenWindowMask];
234 [self changePrimaryWindowToFinalFrame];
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.
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);
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
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
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
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;
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();
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_) {
377 setStyleMask:[primaryWindow_ styleMask] & ~NSFullScreenWindowMask];
378 [self changePrimaryWindowToFinalFrame];
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];
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);