PiP from Element Fullscreen should match AVKit's behavior
[WebKit-https.git] / Source / WebKit / UIProcess / mac / WKFullScreenWindowController.mm
1 /*
2  * Copyright (C) 2009, 2010, 2011 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27
28 #if ENABLE(FULLSCREEN_API) && !PLATFORM(IOS)
29
30 #import "WKFullScreenWindowController.h"
31
32 #import "LayerTreeContext.h"
33 #import "VideoFullscreenManagerProxy.h"
34 #import "WKAPICast.h"
35 #import "WKViewInternal.h"
36 #import "WKViewPrivate.h"
37 #import "WebFullScreenManagerProxy.h"
38 #import "WebPageProxy.h"
39 #import <QuartzCore/QuartzCore.h>
40 #import <WebCore/FloatRect.h>
41 #import <WebCore/GeometryUtilities.h>
42 #import <WebCore/IntRect.h>
43 #import <WebCore/LocalizedStrings.h>
44 #import <WebCore/VideoFullscreenInterfaceMac.h>
45 #import <WebCore/VideoFullscreenModel.h>
46 #import <WebCore/WebCoreFullScreenPlaceholderView.h>
47 #import <WebCore/WebCoreFullScreenWindow.h>
48 #import <pal/system/SleepDisabler.h>
49 #import <wtf/BlockObjCExceptions.h>
50
51 using namespace WebCore;
52
53 static const NSTimeInterval DefaultWatchdogTimerInterval = 1;
54
55 namespace WebKit {
56
57 class WKFullScreenWindowControllerVideoFullscreenModelClient : VideoFullscreenModelClient {
58 public:
59     void setParent(WKFullScreenWindowController *parent) { m_parent = parent; }
60
61     void setInterface(VideoFullscreenInterfaceMac* interface)
62     {
63         if (m_interface == interface)
64             return;
65
66         if (m_interface && m_interface->videoFullscreenModel())
67             m_interface->videoFullscreenModel()->removeClient(*this);
68         m_interface = interface;
69         if (m_interface && m_interface->videoFullscreenModel())
70             m_interface->videoFullscreenModel()->addClient(*this);
71     }
72
73     VideoFullscreenInterfaceMac* interface() const { return m_interface.get(); }
74
75     void didEnterPictureInPicture() final
76     {
77         [m_parent didEnterPictureInPicture];
78     }
79
80 private:
81     WKFullScreenWindowController *m_parent { nullptr };
82     RefPtr<VideoFullscreenInterfaceMac> m_interface;
83 };
84
85 }
86
87 using namespace WebKit;
88
89 enum FullScreenState : NSInteger {
90     NotInFullScreen,
91     WaitingToEnterFullScreen,
92     EnteringFullScreen,
93     InFullScreen,
94     WaitingToExitFullScreen,
95     ExitingFullScreen,
96 };
97
98 @interface NSWindow (WebNSWindowDetails)
99 - (void)exitFullScreenMode:(id)sender;
100 - (void)enterFullScreenMode:(id)sender;
101 @end
102
103 @interface WKFullScreenWindowController (Private) <NSAnimationDelegate>
104 - (void)_replaceView:(NSView*)view with:(NSView*)otherView;
105 - (WebFullScreenManagerProxy*)_manager;
106 - (void)_startEnterFullScreenAnimationWithDuration:(NSTimeInterval)duration;
107 - (void)_startExitFullScreenAnimationWithDuration:(NSTimeInterval)duration;
108 @end
109
110 static NSRect convertRectToScreen(NSWindow *window, NSRect rect)
111 {
112     return [window convertRectToScreen:rect];
113 }
114
115 static void makeResponderFirstResponderIfDescendantOfView(NSWindow *window, NSResponder *responder, NSView *view)
116 {
117     if ([responder isKindOfClass:[NSView class]] && [(NSView *)responder isDescendantOf:view])
118         [window makeFirstResponder:responder];
119 }
120
121 @implementation WKFullScreenWindowController
122
123 #pragma mark -
124 #pragma mark Initialization
125 - (id)initWithWindow:(NSWindow *)window webView:(NSView *)webView page:(WebPageProxy&)page
126 {
127     self = [super initWithWindow:window];
128     if (!self)
129         return nil;
130     [window setDelegate:self];
131     [window setCollectionBehavior:([window collectionBehavior] | NSWindowCollectionBehaviorFullScreenPrimary)];
132
133     NSView *contentView = [window contentView];
134     contentView.hidden = YES;
135     contentView.autoresizesSubviews = YES;
136
137     _backgroundView = adoptNS([[NSView alloc] initWithFrame:contentView.bounds]);
138     _backgroundView.get().layer = [CALayer layer];
139     _backgroundView.get().wantsLayer = YES;
140     _backgroundView.get().autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
141     [contentView addSubview:_backgroundView.get()];
142
143     _clipView = adoptNS([[NSView alloc] initWithFrame:contentView.bounds]);
144     [_clipView setWantsLayer:YES];
145     [_clipView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
146     [_backgroundView addSubview:_clipView.get()];
147
148     [self windowDidLoad];
149     [window displayIfNeeded];
150     _webView = webView;
151     _page = &page;
152
153     _videoFullscreenClient = std::make_unique<WKFullScreenWindowControllerVideoFullscreenModelClient>();
154     _videoFullscreenClient->setParent(self);
155
156     [self videoControlsManagerDidChange];
157
158     return self;
159 }
160
161 - (void)dealloc
162 {
163     [[self window] setDelegate:nil];
164     
165     [NSObject cancelPreviousPerformRequestsWithTarget:self];
166     
167     [[NSNotificationCenter defaultCenter] removeObserver:self];
168
169     if (_repaintCallback) {
170         _repaintCallback->invalidate(WebKit::CallbackBase::Error::OwnerWasInvalidated);
171         // invalidate() calls completeFinishExitFullScreenAnimationAfterRepaint, which
172         // clears _repaintCallback.
173         ASSERT(!_repaintCallback);
174     }
175
176     _videoFullscreenClient->setParent(nil);
177     _videoFullscreenClient->setInterface(nullptr);
178
179     [super dealloc];
180 }
181
182 #pragma mark -
183 #pragma mark Accessors
184
185 @synthesize initialFrame=_initialFrame;
186 @synthesize finalFrame=_finalFrame;
187
188 - (BOOL)isFullScreen
189 {
190     return _fullScreenState == WaitingToEnterFullScreen
191         || _fullScreenState == EnteringFullScreen
192         || _fullScreenState == InFullScreen;
193 }
194
195 - (WebCoreFullScreenPlaceholderView*)webViewPlaceholder
196 {
197     return _webViewPlaceholder.get();
198 }
199
200 - (void)setSavedConstraints:(NSArray *)savedConstraints
201 {
202     _savedConstraints = savedConstraints;
203 }
204
205 - (NSArray *)savedConstraints
206 {
207     return _savedConstraints.get();
208 }
209
210 #pragma mark -
211 #pragma mark NSWindowController overrides
212
213 - (void)cancelOperation:(id)sender
214 {
215     // If the page doesn't respond in DefaultWatchdogTimerInterval seconds, it could be because
216     // the WebProcess has hung, so exit anyway.
217     if (!_watchdogTimer) {
218         [self _manager]->requestExitFullScreen();
219         _watchdogTimer = adoptNS([[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:DefaultWatchdogTimerInterval] interval:0 target:self selector:@selector(_watchdogTimerFired:) userInfo:nil repeats:NO]);
220         [[NSRunLoop mainRunLoop] addTimer:_watchdogTimer.get() forMode:NSDefaultRunLoopMode];
221     }
222 }
223
224 #pragma mark -
225 #pragma mark Exposed Interface
226
227 static RetainPtr<CGDataProviderRef> createImageProviderWithCopiedData(CGDataProviderRef sourceProvider)
228 {
229     RetainPtr<CFDataRef> data = adoptCF(CGDataProviderCopyData(sourceProvider));
230     return adoptCF(CGDataProviderCreateWithCFData(data.get()));
231 }
232
233 static RetainPtr<CGImageRef> createImageWithCopiedData(CGImageRef sourceImage)
234 {
235     size_t width = CGImageGetWidth(sourceImage);
236     size_t height = CGImageGetHeight(sourceImage);
237     size_t bitsPerComponent = CGImageGetBitsPerComponent(sourceImage);
238     size_t bitsPerPixel = CGImageGetBitsPerPixel(sourceImage);
239     size_t bytesPerRow = CGImageGetBytesPerRow(sourceImage);
240     CGColorSpaceRef colorSpace = CGImageGetColorSpace(sourceImage);
241     CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(sourceImage);
242     RetainPtr<CGDataProviderRef> provider = createImageProviderWithCopiedData(CGImageGetDataProvider(sourceImage));
243     bool shouldInterpolate = CGImageGetShouldInterpolate(sourceImage);
244     CGColorRenderingIntent intent = CGImageGetRenderingIntent(sourceImage);
245
246     return adoptCF(CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpace, bitmapInfo, provider.get(), 0, shouldInterpolate, intent));
247 }
248
249 - (void)enterFullScreen:(NSScreen *)screen
250 {
251     if ([self isFullScreen])
252         return;
253     _fullScreenState = WaitingToEnterFullScreen;
254
255     if (!screen)
256         screen = [NSScreen mainScreen];
257     NSRect screenFrame = [screen frame];
258
259     NSRect webViewFrame = convertRectToScreen([_webView window], [_webView convertRect:[_webView frame] toView:nil]);
260
261     // Flip coordinate system:
262     webViewFrame.origin.y = NSMaxY([[[NSScreen screens] objectAtIndex:0] frame]) - NSMaxY(webViewFrame);
263
264     CGWindowID windowID = [[_webView window] windowNumber];
265     RetainPtr<CGImageRef> webViewContents = adoptCF(CGWindowListCreateImage(NSRectToCGRect(webViewFrame), kCGWindowListOptionIncludingWindow, windowID, kCGWindowImageShouldBeOpaque));
266
267     // Using the returned CGImage directly would result in calls to the WindowServer every time
268     // the image was painted. Instead, copy the image data into our own process to eliminate that
269     // future overhead.
270     webViewContents = createImageWithCopiedData(webViewContents.get());
271
272 #pragma clang diagnostic push
273 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
274     // Screen updates to be re-enabled in _startEnterFullScreenAnimationWithDuration:
275     NSDisableScreenUpdates();
276     [[self window] setAutodisplay:NO];
277 #pragma clang diagnostic pop
278
279     [self _manager]->saveScrollPosition();
280     _savedTopContentInset = _page->topContentInset();
281     _page->setTopContentInset(0);
282     [[self window] setFrame:screenFrame display:NO];
283
284     // Painting is normally suspended when the WKView is removed from the window, but this is
285     // unnecessary in the full-screen animation case, and can cause bugs; see
286     // https://bugs.webkit.org/show_bug.cgi?id=88940 and https://bugs.webkit.org/show_bug.cgi?id=88374
287     // We will resume the normal behavior in _startEnterFullScreenAnimationWithDuration:
288     _page->setSuppressVisibilityUpdates(true);
289
290     // Swap the webView placeholder into place.
291     if (!_webViewPlaceholder) {
292         _webViewPlaceholder = adoptNS([[WebCoreFullScreenPlaceholderView alloc] initWithFrame:[_webView frame]]);
293         [_webViewPlaceholder setAction:@selector(cancelOperation:)];
294     }
295     [_webViewPlaceholder setTarget:nil];
296     [_webViewPlaceholder setContents:(__bridge id)webViewContents.get()];
297     self.savedConstraints = _webView.superview.constraints;
298     [self _replaceView:_webView with:_webViewPlaceholder.get()];
299     
300     // Then insert the WebView into the full screen window
301     NSView *contentView = [[self window] contentView];
302     [_clipView addSubview:_webView positioned:NSWindowBelow relativeTo:nil];
303     _webView.frame = NSInsetRect(contentView.bounds, 0, -_page->topContentInset());
304
305     _savedScale = _page->pageScaleFactor();
306     _page->scalePage(1, IntPoint());
307     [self _manager]->setAnimatingFullScreen(true);
308     [self _manager]->willEnterFullScreen();
309 }
310
311 - (void)beganEnterFullScreenWithInitialFrame:(NSRect)initialFrame finalFrame:(NSRect)finalFrame
312 {
313     if (_fullScreenState != WaitingToEnterFullScreen)
314         return;
315     _fullScreenState = EnteringFullScreen;
316
317     _initialFrame = initialFrame;
318     _finalFrame = finalFrame;
319
320     [self.window orderBack: self]; // Make sure the full screen window is part of the correct Space.
321     [[self window] enterFullScreenMode:self];
322 }
323
324 static const float minVideoWidth = 480 + 20 + 20; // Note: Keep in sync with mediaControlsApple.css (video:-webkit-full-screen::-webkit-media-controls-panel)
325
326 - (void)finishedEnterFullScreenAnimation:(bool)completed
327 {
328     if (_fullScreenState != EnteringFullScreen)
329         return;
330     
331     if (completed) {
332         _fullScreenState = InFullScreen;
333
334 #pragma clang diagnostic push
335 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
336         // Screen updates to be re-enabled ta the end of the current block.
337         NSDisableScreenUpdates();
338 #pragma clang diagnostic pop
339         [self _manager]->didEnterFullScreen();
340         [self _manager]->setAnimatingFullScreen(false);
341
342         [_backgroundView.get().layer removeAllAnimations];
343         [[_clipView layer] removeAllAnimations];
344         [[_clipView layer] setMask:nil];
345
346         [_webViewPlaceholder setExitWarningVisible:YES];
347         [_webViewPlaceholder setTarget:self];
348
349         NSSize minContentSize = self.window.contentMinSize;
350         minContentSize.width = minVideoWidth;
351         self.window.contentMinSize = minContentSize;
352     } else {
353         // Transition to fullscreen failed. Clean up.
354         _fullScreenState = NotInFullScreen;
355
356 #pragma clang diagnostic push
357 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
358         [[self window] setAutodisplay:YES];
359 #pragma clang diagnostic pop
360         _page->setSuppressVisibilityUpdates(false);
361
362         NSResponder *firstResponder = [[self window] firstResponder];
363         [self _replaceView:_webViewPlaceholder.get() with:_webView];
364         BEGIN_BLOCK_OBJC_EXCEPTIONS
365         [NSLayoutConstraint activateConstraints:self.savedConstraints];
366         END_BLOCK_OBJC_EXCEPTIONS
367         self.savedConstraints = nil;
368         makeResponderFirstResponderIfDescendantOfView(_webView.window, firstResponder, _webView);
369         [[_webView window] makeKeyAndOrderFront:self];
370
371         _page->scalePage(_savedScale, IntPoint());
372         [self _manager]->restoreScrollPosition();
373         _page->setTopContentInset(_savedTopContentInset);
374         [self _manager]->didExitFullScreen();
375         [self _manager]->setAnimatingFullScreen(false);
376     }
377
378 #pragma clang diagnostic push
379 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
380     NSEnableScreenUpdates();
381 #pragma clang diagnostic pop
382
383     if (_requestedExitFullScreen) {
384         _requestedExitFullScreen = NO;
385         [self exitFullScreen];
386     }
387 }
388
389 - (void)exitFullScreen
390 {
391     if (_fullScreenState == EnteringFullScreen
392         || _fullScreenState == WaitingToEnterFullScreen) {
393         // Do not try to exit fullscreen during the enter animation; remember
394         // that exit was requested and perform the exit upon enter fullscreen
395         // animation complete.
396         _requestedExitFullScreen = YES;
397         return;
398     }
399
400     if (_watchdogTimer) {
401         [_watchdogTimer invalidate];
402         _watchdogTimer.clear();
403     }
404
405     if (![self isFullScreen])
406         return;
407     _fullScreenState = WaitingToExitFullScreen;
408
409     [_webViewPlaceholder setExitWarningVisible:NO];
410
411 #pragma clang diagnostic push
412 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
413     // Screen updates to be re-enabled in _startExitFullScreenAnimationWithDuration: or beganExitFullScreenWithInitialFrame:finalFrame:
414     NSDisableScreenUpdates();
415     [[self window] setAutodisplay:NO];
416 #pragma clang diagnostic pop
417
418     // See the related comment in enterFullScreen:
419     // We will resume the normal behavior in _startExitFullScreenAnimationWithDuration:
420     _page->setSuppressVisibilityUpdates(true);
421     [_webViewPlaceholder setTarget:nil];
422
423     [self _manager]->setAnimatingFullScreen(true);
424     [self _manager]->willExitFullScreen();
425 }
426
427 - (void)requestExitFullScreen
428 {
429     [self _manager]->requestExitFullScreen();
430 }
431
432 - (void)beganExitFullScreenWithInitialFrame:(NSRect)initialFrame finalFrame:(NSRect)finalFrame
433 {
434     if (_fullScreenState != WaitingToExitFullScreen)
435         return;
436     _fullScreenState = ExitingFullScreen;
437
438     if (![[self window] isOnActiveSpace]) {
439         // If the full screen window is not in the active space, the NSWindow full screen animation delegate methods
440         // will never be called. So call finishedExitFullScreenAnimation explicitly.
441         [self finishedExitFullScreenAnimation:YES];
442
443         // Because we are breaking the normal animation pattern, re-enable screen updates
444         // as exitFullScreen has disabled them, but _startExitFullScreenAnimationWithDuration:
445         // will never be called.
446 #pragma clang diagnostic push
447 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
448         NSEnableScreenUpdates();
449 #pragma clang diagnostic pop
450     }
451
452     [[self window] exitFullScreenMode:self];
453 }
454
455 - (void)finishedExitFullScreenAnimation:(bool)completed
456 {
457     if (_fullScreenState == InFullScreen) {
458         // If we are currently in the InFullScreen state, this notification is unexpected, meaning
459         // fullscreen was exited without being initiated by WebKit. Do not return early, but continue to
460         // clean up our state by calling those methods which would have been called by -exitFullscreen,
461         // and proceed to close the fullscreen window.
462         [self _manager]->requestExitFullScreen();
463         [_webViewPlaceholder setTarget:nil];
464         [self _manager]->setAnimatingFullScreen(false);
465         [self _manager]->willExitFullScreen();
466     } else if (_fullScreenState != ExitingFullScreen)
467         return;
468     _fullScreenState = NotInFullScreen;
469
470     NSResponder *firstResponder = [[self window] firstResponder];
471
472 #pragma clang diagnostic push
473 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
474     // Screen updates to be re-enabled in completeFinishExitFullScreenAnimationAfterRepaint.
475     NSDisableScreenUpdates();
476 #pragma clang diagnostic pop
477     _page->setSuppressVisibilityUpdates(true);
478     [[self window] orderOut:self];
479     NSView *contentView = [[self window] contentView];
480     contentView.hidden = YES;
481     [_backgroundView.get().layer removeAllAnimations];
482 #pragma clang diagnostic push
483 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
484     [[_webViewPlaceholder window] setAutodisplay:NO];
485 #pragma clang diagnostic pop
486
487     [self _replaceView:_webViewPlaceholder.get() with:_webView];
488     BEGIN_BLOCK_OBJC_EXCEPTIONS
489     [NSLayoutConstraint activateConstraints:self.savedConstraints];
490     END_BLOCK_OBJC_EXCEPTIONS
491     self.savedConstraints = nil;
492     makeResponderFirstResponderIfDescendantOfView(_webView.window, firstResponder, _webView);
493
494     [[_webView window] makeKeyAndOrderFront:self];
495
496     // These messages must be sent after the swap or flashing will occur during forceRepaint:
497     [self _manager]->didExitFullScreen();
498     [self _manager]->setAnimatingFullScreen(false);
499     _page->scalePage(_savedScale, IntPoint());
500     [self _manager]->restoreScrollPosition();
501     _page->setTopContentInset(_savedTopContentInset);
502
503     if (_repaintCallback) {
504         _repaintCallback->invalidate(WebKit::CallbackBase::Error::OwnerWasInvalidated);
505         // invalidate() calls completeFinishExitFullScreenAnimationAfterRepaint, which
506         // clears _repaintCallback.
507         ASSERT(!_repaintCallback);
508     }
509     _repaintCallback = VoidCallback::create([self](WebKit::CallbackBase::Error) {
510         [self completeFinishExitFullScreenAnimationAfterRepaint];
511     });
512     _page->forceRepaint(_repaintCallback.copyRef());
513 }
514
515 - (void)completeFinishExitFullScreenAnimationAfterRepaint
516 {
517     _repaintCallback = nullptr;
518 #pragma clang diagnostic push
519 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
520     [[_webView window] setAutodisplay:YES];
521 #pragma clang diagnostic pop
522     [[_webView window] displayIfNeeded];
523     _page->setSuppressVisibilityUpdates(false);
524 #pragma clang diagnostic push
525 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
526     NSEnableScreenUpdates();
527 #pragma clang diagnostic pop
528 }
529
530 - (void)performClose:(id)sender
531 {
532     if ([self isFullScreen])
533         [self cancelOperation:sender];
534 }
535
536 - (void)close
537 {
538     // We are being asked to close rapidly, most likely because the page 
539     // has closed or the web process has crashed.  Just walk through our
540     // normal exit full screen sequence, but don't wait to be called back
541     // in response.
542     if ([self isFullScreen])
543         [self exitFullScreen];
544     
545     if (_fullScreenState == ExitingFullScreen)
546         [self finishedExitFullScreenAnimation:YES];
547
548     _webView = nil;
549
550     [super close];
551 }
552
553 - (void)videoControlsManagerDidChange
554 {
555     auto* videoFullscreenManager = _page ? _page->videoFullscreenManager() : nullptr;
556     auto* videoFullscreenInterface = videoFullscreenManager ? videoFullscreenManager->controlsManagerInterface() : nullptr;
557
558     _videoFullscreenClient->setInterface(videoFullscreenInterface);
559 }
560
561 - (void)didEnterPictureInPicture
562 {
563     [self requestExitFullScreen];
564 }
565
566 #pragma mark -
567 #pragma mark Custom NSWindow Full Screen Animation
568
569 - (NSArray *)customWindowsToEnterFullScreenForWindow:(NSWindow *)window
570 {
571     return @[self.window];
572 }
573
574 - (NSArray *)customWindowsToExitFullScreenForWindow:(NSWindow *)window
575 {
576     return @[self.window];
577 }
578
579 - (void)window:(NSWindow *)window startCustomAnimationToEnterFullScreenWithDuration:(NSTimeInterval)duration
580 {
581     [self _startEnterFullScreenAnimationWithDuration:duration];
582 }
583
584 - (void)window:(NSWindow *)window startCustomAnimationToExitFullScreenWithDuration:(NSTimeInterval)duration
585 {
586     [self _startExitFullScreenAnimationWithDuration:duration];
587 }
588
589 - (void)windowDidFailToEnterFullScreen:(NSWindow *)window
590 {
591     [self finishedEnterFullScreenAnimation:NO];
592 }
593
594 - (void)windowDidEnterFullScreen:(NSNotification*)notification
595 {
596     [self finishedEnterFullScreenAnimation:YES];
597 }
598
599 - (void)windowDidFailToExitFullScreen:(NSWindow *)window
600 {
601     [self finishedExitFullScreenAnimation:NO];
602 }
603
604 - (void)windowDidExitFullScreen:(NSNotification*)notification
605 {
606     [self finishedExitFullScreenAnimation:YES];
607 }
608
609 - (NSWindow *)destinationWindowToExitFullScreenForWindow:(NSWindow *)window
610 {
611     return self.webViewPlaceholder.window;
612 }
613
614 #pragma mark -
615 #pragma mark Internal Interface
616
617 - (WebFullScreenManagerProxy*)_manager
618 {
619     if (!_page)
620         return nullptr;
621     return _page->fullScreenManager();
622 }
623
624 - (void)_replaceView:(NSView*)view with:(NSView*)otherView
625 {
626     [CATransaction begin];
627     [CATransaction setDisableActions:YES];
628     [otherView setFrame:[view frame]];        
629     [otherView setAutoresizingMask:[view autoresizingMask]];
630     [otherView removeFromSuperview];
631     [[view superview] addSubview:otherView positioned:NSWindowAbove relativeTo:view];
632     [view removeFromSuperview];
633     [CATransaction commit];
634 }
635
636 static CAMediaTimingFunction *timingFunctionForDuration(CFTimeInterval duration)
637 {
638     if (duration >= 0.8)
639         return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
640     return [CAMediaTimingFunction functionWithControlPoints:.25 :0 :0 :1];
641 }
642
643 enum AnimationDirection { AnimateIn, AnimateOut };
644 static CAAnimation *zoomAnimation(const FloatRect& initialFrame, const FloatRect& finalFrame, const FloatRect& screenFrame, CFTimeInterval duration, AnimationDirection direction)
645 {
646     CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
647     FloatRect scaleRect = smallestRectWithAspectRatioAroundRect(finalFrame.size().aspectRatio(), initialFrame);
648     CGAffineTransform resetOriginTransform = CGAffineTransformMakeTranslation(screenFrame.x() - finalFrame.x(), screenFrame.y() - finalFrame.y());
649     CGAffineTransform scaleTransform = CGAffineTransformMakeScale(scaleRect.width() / finalFrame.width(), scaleRect.height() / finalFrame.height());
650     CGAffineTransform translateTransform = CGAffineTransformMakeTranslation(scaleRect.x() - screenFrame.x(), scaleRect.y() - screenFrame.y());
651
652     CGAffineTransform finalTransform = CGAffineTransformConcat(CGAffineTransformConcat(resetOriginTransform, scaleTransform), translateTransform);
653     NSValue *scaleValue = [NSValue valueWithCATransform3D:CATransform3DMakeAffineTransform(finalTransform)];
654     if (direction == AnimateIn)
655         scaleAnimation.fromValue = scaleValue;
656     else
657         scaleAnimation.toValue = scaleValue;
658
659     scaleAnimation.duration = duration;
660     scaleAnimation.removedOnCompletion = NO;
661     scaleAnimation.fillMode = kCAFillModeBoth;
662     scaleAnimation.timingFunction = timingFunctionForDuration(duration);
663     return scaleAnimation;
664 }
665
666 static CALayer *createMask(const FloatRect& bounds)
667 {
668     CALayer *maskLayer = [CALayer layer];
669     maskLayer.anchorPoint = CGPointZero;
670     maskLayer.frame = bounds;
671     maskLayer.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
672     maskLayer.autoresizingMask = (NSViewWidthSizable | NSViewHeightSizable);
673     return maskLayer;
674 }
675
676 static CAAnimation *maskAnimation(const FloatRect& initialFrame, const FloatRect& finalFrame, const FloatRect& screenFrame, CFTimeInterval duration, AnimationDirection direction)
677 {
678     CABasicAnimation *boundsAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"];
679     FloatRect boundsRect = largestRectWithAspectRatioInsideRect(initialFrame.size().aspectRatio(), finalFrame);
680     NSValue *boundsValue = [NSValue valueWithRect:FloatRect(FloatPoint(), boundsRect.size())];
681     if (direction == AnimateIn)
682         boundsAnimation.fromValue = boundsValue;
683     else
684         boundsAnimation.toValue = boundsValue;
685
686     CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
687     NSValue *positionValue = [NSValue valueWithPoint:FloatPoint(boundsRect.location() - screenFrame.location())];
688     if (direction == AnimateIn)
689         positionAnimation.fromValue = positionValue;
690     else
691         positionAnimation.toValue = positionValue;
692
693     CAAnimationGroup *animation = [CAAnimationGroup animation];
694     animation.animations = @[boundsAnimation, positionAnimation];
695     animation.duration = duration;
696     animation.removedOnCompletion = NO;
697     animation.fillMode = kCAFillModeBoth;
698     animation.timingFunction = timingFunctionForDuration(duration);
699     return animation;
700 }
701
702 static CAAnimation *fadeAnimation(CFTimeInterval duration, AnimationDirection direction)
703 {
704     CABasicAnimation *fadeAnimation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
705     if (direction == AnimateIn)
706         fadeAnimation.toValue = (id)CGColorGetConstantColor(kCGColorBlack);
707     else
708         fadeAnimation.fromValue = (id)CGColorGetConstantColor(kCGColorBlack);
709     fadeAnimation.duration = duration;
710     fadeAnimation.removedOnCompletion = NO;
711     fadeAnimation.fillMode = kCAFillModeBoth;
712     fadeAnimation.timingFunction = timingFunctionForDuration(duration);
713     return fadeAnimation;
714 }
715
716 - (void)_startEnterFullScreenAnimationWithDuration:(NSTimeInterval)duration
717 {
718     NSView* contentView = [[self window] contentView];
719
720     [[_clipView layer] addAnimation:zoomAnimation(_initialFrame, _finalFrame, self.window.screen.frame, duration, AnimateIn) forKey:@"fullscreen"];
721     CALayer *maskLayer = createMask(contentView.bounds);
722     [maskLayer addAnimation:maskAnimation(_initialFrame, _finalFrame, self.window.screen.frame, duration, AnimateIn) forKey:@"fullscreen"];
723     [_clipView layer].mask = maskLayer;
724
725     contentView.hidden = NO;
726     [_backgroundView.get().layer addAnimation:fadeAnimation(duration, AnimateIn) forKey:@"fullscreen"];
727
728     NSWindow* window = [self window];
729     NSWindowCollectionBehavior behavior = [window collectionBehavior];
730     [window setCollectionBehavior:(behavior | NSWindowCollectionBehaviorCanJoinAllSpaces)];
731     [window makeKeyAndOrderFront:self];
732     [window setCollectionBehavior:behavior];
733     [window makeFirstResponder:_webView];
734
735     _page->setSuppressVisibilityUpdates(false);
736 #pragma clang diagnostic push
737 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
738     [[self window] setAutodisplay:YES];
739 #pragma clang diagnostic pop
740     [[self window] displayIfNeeded];
741 #pragma clang diagnostic push
742 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
743     NSEnableScreenUpdates();
744 #pragma clang diagnostic pop
745 }
746
747 - (void)_startExitFullScreenAnimationWithDuration:(NSTimeInterval)duration
748 {
749     if ([self isFullScreen]) {
750         // We still believe we're in full screen mode, so we must have been asked to exit full
751         // screen by the system full screen button.
752         [self _manager]->requestExitFullScreen();
753         [self exitFullScreen];
754         _fullScreenState = ExitingFullScreen;
755     }
756
757     [[_clipView layer] addAnimation:zoomAnimation(_initialFrame, _finalFrame, self.window.screen.frame, duration, AnimateOut) forKey:@"fullscreen"];
758     NSView* contentView = [[self window] contentView];
759     CALayer *maskLayer = createMask(contentView.bounds);
760     [maskLayer addAnimation:maskAnimation(_initialFrame, _finalFrame, self.window.screen.frame, duration, AnimateOut) forKey:@"fullscreen"];
761     [_clipView layer].mask = maskLayer;
762
763     contentView.hidden = NO;
764     [_backgroundView.get().layer addAnimation:fadeAnimation(duration, AnimateOut) forKey:@"fullscreen"];
765
766     _page->setSuppressVisibilityUpdates(false);
767 #pragma clang diagnostic push
768 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
769     [[self window] setAutodisplay:YES];
770 #pragma clang diagnostic pop
771     [[self window] displayIfNeeded];
772 #pragma clang diagnostic push
773 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
774     NSEnableScreenUpdates();
775 #pragma clang diagnostic pop
776 }
777
778 - (void)_watchdogTimerFired:(NSTimer *)timer
779 {
780     [self exitFullScreen];
781 }
782
783 @end
784
785 #endif // ENABLE(FULLSCREEN_API) && !PLATFORM(IOS)