// 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