| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/40285824): Remove this and convert code to safer constructs. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #import "ui/base/cocoa/constrained_window/constrained_window_animation.h" |
| |
| #include <stdint.h> |
| #include <stdlib.h> |
| |
| #import "base/apple/foundation_util.h" |
| #include "base/files/file_path.h" |
| #include "base/location.h" |
| #include "base/native_library.h" |
| #include "base/notreached.h" |
| #include "ui/gfx/animation/tween.h" |
| |
| // The window animations in this file use private APIs as described here: |
| // https://github.com/MarkVillacampa/undocumented-goodness/blob/master/CoreGraphics/CGSPrivate.h |
| // There are two important things to keep in mind when modifying this file: |
| // - For most operations the origin of the coordinate system is top left. |
| // - Perspective and shear transformations get clipped if they are bigger |
| // than the window size. This does not seem to apply to scale transformations. |
| |
| // Length of the animation in seconds. |
| const NSTimeInterval kAnimationDuration = 0.18; |
| |
| // The number of pixels above the final destination to animate from. |
| const CGFloat kShowHideVerticalOffset = 20; |
| |
| // Scale the window by this factor when animating. |
| const CGFloat kShowHideScaleFactor = 0.99; |
| |
| // Size of the perspective effect as a factor of the window width. |
| const CGFloat kShowHidePerspectiveFactor = 0.04; |
| |
| // Forward declare private CoreGraphics APIs used to transform windows. |
| extern "C" { |
| |
| typedef float float32; |
| |
| typedef int32_t CGSWindow; |
| typedef int32_t CGSConnection; |
| |
| typedef struct { |
| float32 x; |
| float32 y; |
| } MeshPoint; |
| |
| typedef struct { |
| MeshPoint local; |
| MeshPoint global; |
| } CGPointWarp; |
| |
| CGSConnection _CGSDefaultConnection(); |
| CGError CGSSetWindowTransform(const CGSConnection cid, |
| const CGSWindow wid, |
| CGAffineTransform transform); |
| CGError CGSSetWindowWarp(const CGSConnection cid, |
| const CGSWindow wid, |
| int32_t w, |
| int32_t h, |
| CGPointWarp* mesh); |
| CGError CGSSetWindowAlpha(const CGSConnection cid, |
| const CGSWindow wid, |
| float32 alpha); |
| |
| } // extern "C" |
| |
| namespace { |
| |
| struct KeyFrame { |
| float value; |
| float scale; |
| }; |
| |
| // Get the window location relative to the top left of the main screen. |
| // Most Cocoa APIs use a coordinate system where the screen origin is the |
| // bottom left. The various CGSSetWindow* APIs use a coordinate system where |
| // the screen origin is the top left. |
| NSPoint GetCGSWindowScreenOrigin(NSWindow* window) { |
| NSArray* screens = NSScreen.screens; |
| if (screens.count == 0) { |
| return NSZeroPoint; |
| } |
| // Origin is relative to the screen with the menu bar (the screen at index 0). |
| // Note, this is not the same as mainScreen which is the screen with the key |
| // window. |
| NSScreen* main_screen = screens[0]; |
| |
| NSRect window_frame = window.frame; |
| NSRect screen_frame = main_screen.frame; |
| return NSMakePoint(NSMinX(window_frame), |
| NSHeight(screen_frame) - NSMaxY(window_frame)); |
| } |
| |
| // Set the transparency of the window. |
| void SetWindowAlpha(NSWindow* window, float alpha) { |
| CGSConnection cid = _CGSDefaultConnection(); |
| CGSSetWindowAlpha(cid, static_cast<CGSWindow>(window.windowNumber), alpha); |
| } |
| |
| // Scales the window and translates it so that it stays centered relative |
| // to its original position. |
| void SetWindowScale(NSWindow* window, float scale) { |
| CGFloat scale_delta = 1.0 - scale; |
| CGFloat cur_scale = 1.0 + scale_delta; |
| CGAffineTransform transform = |
| CGAffineTransformMakeScale(cur_scale, cur_scale); |
| |
| // Translate the window to keep it centered at the original location. |
| NSSize window_size = window.frame.size; |
| CGFloat scale_offset_x = window_size.width * (1 - cur_scale) / 2.0; |
| CGFloat scale_offset_y = window_size.height * (1 - cur_scale) / 2.0; |
| |
| NSPoint origin = GetCGSWindowScreenOrigin(window); |
| CGFloat new_x = -origin.x + scale_offset_x; |
| CGFloat new_y = -origin.y + scale_offset_y; |
| transform = CGAffineTransformTranslate(transform, new_x, new_y); |
| |
| CGSConnection cid = _CGSDefaultConnection(); |
| CGSSetWindowTransform(cid, static_cast<CGSWindow>(window.windowNumber), |
| transform); |
| } |
| |
| // Unsets any window warp that may have been previously applied. |
| // Window warp prevents other effects such as CGSSetWindowTransform from |
| // being applied. |
| void ClearWindowWarp(NSWindow* window) { |
| CGSConnection cid = _CGSDefaultConnection(); |
| CGSSetWindowWarp(cid, static_cast<CGSWindow>(window.windowNumber), 0, 0, |
| nullptr); |
| } |
| |
| // Applies various transformations using a warp effect. The window is |
| // translated vertically by |y_offset|. The window is scaled by |scale| and |
| // translated so that the it remains centered relative to its original position. |
| // Finally, perspective is effect is applied by shrinking the top of the window. |
| void SetWindowWarp(NSWindow* window, |
| float y_offset, |
| float scale, |
| float perspective_offset) { |
| NSRect win_rect = window.frame; |
| win_rect.origin = NSZeroPoint; |
| NSRect screen_rect = win_rect; |
| screen_rect.origin = GetCGSWindowScreenOrigin(window); |
| |
| // Apply a vertical translate. |
| screen_rect.origin.y -= y_offset; |
| |
| // Apply a scale and translate to keep the window centered. |
| screen_rect.origin.x += (NSWidth(win_rect) - NSWidth(screen_rect)) / 2.0; |
| screen_rect.origin.y += (NSHeight(win_rect) - NSHeight(screen_rect)) / 2.0; |
| |
| // A 2 x 2 mesh that maps each corner of the window to a location in screen |
| // coordinates. Note that the origin of the coordinate system is top, left. |
| CGPointWarp mesh[2][2] = { |
| {{ |
| // Top left. |
| {static_cast<float>(NSMinX(win_rect)), |
| static_cast<float>(NSMinY(win_rect))}, |
| {static_cast<float>(NSMinX(screen_rect) + perspective_offset), |
| static_cast<float>(NSMinY(screen_rect))}, |
| }, |
| { |
| // Top right. |
| {static_cast<float>(NSMaxX(win_rect)), |
| static_cast<float>(NSMinY(win_rect))}, |
| {static_cast<float>(NSMaxX(screen_rect) - perspective_offset), |
| static_cast<float>(NSMinY(screen_rect))}, |
| }}, |
| {{ |
| // Bottom left. |
| {static_cast<float>(NSMinX(win_rect)), |
| static_cast<float>(NSMaxY(win_rect))}, |
| {static_cast<float>(NSMinX(screen_rect)), |
| static_cast<float>(NSMaxY(screen_rect))}, |
| }, |
| { |
| // Bottom right. |
| {static_cast<float>(NSMaxX(win_rect)), |
| static_cast<float>(NSMaxY(win_rect))}, |
| {static_cast<float>(NSMaxX(screen_rect)), |
| static_cast<float>(NSMaxY(screen_rect))}, |
| }}, |
| }; |
| |
| CGSConnection cid = _CGSDefaultConnection(); |
| CGSSetWindowWarp(cid, static_cast<CGSWindow>(window.windowNumber), 2, 2, |
| &(mesh[0][0])); |
| } |
| |
| // Sets the various effects that are a part of the Show/Hide animation. |
| // Value is a number between 0 and 1 where 0 means the window is completely |
| // hidden and 1 means the window is fully visible. |
| void UpdateWindowShowHideAnimationState(NSWindow* window, CGFloat value) { |
| CGFloat inverse_value = 1.0 - value; |
| |
| SetWindowAlpha(window, value); |
| CGFloat y_offset = kShowHideVerticalOffset * inverse_value; |
| CGFloat scale = 1.0 - (1.0 - kShowHideScaleFactor) * inverse_value; |
| CGFloat perspective_offset = |
| (window.frame.size.width * kShowHidePerspectiveFactor) * inverse_value; |
| |
| SetWindowWarp(window, y_offset, scale, perspective_offset); |
| } |
| |
| bool AreWindowServerEffectsDisabled() { |
| // If the CHROME_HEADLESS env variable is set, this code is running in a |
| // test environment. The custom constrained window animations may be |
| // causing the WindowServer to crash (https://crbug.com/828031), so use the |
| // simple animations. |
| static bool is_headless = getenv("CHROME_HEADLESS") != nullptr; |
| return is_headless; |
| } |
| |
| } // namespace |
| |
| @interface ConstrainedWindowAnimationBase () |
| // Subclasses should override these to update the window state for the current |
| // animation value. |
| - (void)setWindowStateForStart; |
| - (void)setWindowStateForValue:(float)value; |
| - (void)setWindowStateForEnd; |
| |
| @property(strong) NSWindow* window; |
| |
| @end |
| |
| @implementation ConstrainedWindowAnimationBase |
| |
| @synthesize window = _window; |
| |
| - (instancetype)initWithWindow:(NSWindow*)window { |
| if ((self = [self initWithDuration:kAnimationDuration |
| animationCurve:NSAnimationEaseInOut])) { |
| self.window = window; |
| self.animationBlockingMode = NSAnimationBlocking; |
| [self setWindowStateForStart]; |
| } |
| return self; |
| } |
| |
| - (void)stopAnimation { |
| [super stopAnimation]; |
| [self setWindowStateForEnd]; |
| if ([self.delegate respondsToSelector:@selector(animationDidEnd:)]) { |
| [self.delegate animationDidEnd:self]; |
| } |
| } |
| |
| - (void)setCurrentProgress:(NSAnimationProgress)progress { |
| [super setCurrentProgress:progress]; |
| |
| if (progress >= 1.0) { |
| [self setWindowStateForEnd]; |
| |
| // Starting in 10.10, the WindowServer forgets to draw the shadow on windows |
| // that animate in this way on retina screens. -[NSWindow invalidateShadow] |
| // doesn't fix it. Neither does toggling -setHasShadow:. But forcing an |
| // update to the window size, and then undoing it, seems to fix the problem. |
| // See http://crbug.com/436884. |
| // TODO(tapted): Find a better fix (this is horrible). |
| if (!AreWindowServerEffectsDisabled()) { |
| NSRect frame = self.window.frame; |
| [self.window setFrame:NSInsetRect(frame, 1, 1) display:NO animate:NO]; |
| [self.window setFrame:frame display:NO animate:NO]; |
| } |
| return; |
| } |
| [self setWindowStateForValue:[self currentValue]]; |
| } |
| |
| - (void)setWindowStateForStart { |
| // Subclasses can optionally override this method. |
| } |
| |
| - (void)setWindowStateForValue:(float)value { |
| // Subclasses must override this method. |
| NOTREACHED(); |
| } |
| |
| - (void)setWindowStateForEnd { |
| // Subclasses can optionally override this method. |
| } |
| |
| @end |
| |
| @implementation ConstrainedWindowAnimationShow |
| |
| - (void)setWindowStateForStart { |
| if (AreWindowServerEffectsDisabled()) { |
| self.window.alphaValue = 0.0; |
| return; |
| } |
| SetWindowAlpha(self.window, 0.0); |
| } |
| |
| - (void)setWindowStateForValue:(float)value { |
| if (AreWindowServerEffectsDisabled()) { |
| self.window.alphaValue = value; |
| return; |
| } |
| UpdateWindowShowHideAnimationState(self.window, value); |
| } |
| |
| - (void)setWindowStateForEnd { |
| if (AreWindowServerEffectsDisabled()) { |
| self.window.alphaValue = 1.0; |
| return; |
| } |
| SetWindowAlpha(self.window, 1.0); |
| ClearWindowWarp(self.window); |
| } |
| |
| @end |
| |
| @implementation ConstrainedWindowAnimationHide |
| |
| - (void)setWindowStateForValue:(float)value { |
| if (AreWindowServerEffectsDisabled()) { |
| self.window.alphaValue = 1.0 - value; |
| return; |
| } |
| UpdateWindowShowHideAnimationState(self.window, 1.0 - value); |
| } |
| |
| - (void)setWindowStateForEnd { |
| if (AreWindowServerEffectsDisabled()) { |
| self.window.alphaValue = 0.0; |
| return; |
| } |
| SetWindowAlpha(self.window, 0.0); |
| ClearWindowWarp(self.window); |
| } |
| |
| @end |
| |
| @implementation ConstrainedWindowAnimationPulse |
| |
| // Sets the window scale based on the animation progress. |
| - (void)setWindowStateForValue:(float)value { |
| if (AreWindowServerEffectsDisabled()) |
| return; |
| |
| KeyFrame frames[] = { |
| {0.00, 1.0}, {0.40, 1.02}, {0.60, 1.02}, {1.00, 1.0}, |
| }; |
| |
| CGFloat scale = 1; |
| for (int i = std::size(frames) - 1; i >= 0; --i) { |
| if (value >= frames[i].value) { |
| CGFloat delta = frames[i + 1].value - frames[i].value; |
| CGFloat frame_progress = (value - frames[i].value) / delta; |
| scale = gfx::Tween::FloatValueBetween(frame_progress, frames[i].scale, |
| frames[i + 1].scale); |
| break; |
| } |
| } |
| |
| SetWindowScale(self.window, scale); |
| } |
| |
| - (void)setWindowStateForEnd { |
| if (AreWindowServerEffectsDisabled()) { |
| NSBeep(); |
| return; |
| } |
| |
| SetWindowScale(self.window, 1.0); |
| } |
| |
| @end |