BookmarkManager: Fix 'new folder text field size changes on clicking it' issue.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / spinner_view.mm
blob0134050621d78595d5040852fc6797f2968d8d1e
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/spinner_view.h"
7 #import <QuartzCore/QuartzCore.h>
9 #include "base/mac/mac_util.h"
10 #include "base/mac/sdk_forward_declarations.h"
11 #include "base/mac/scoped_cftyperef.h"
12 #include "skia/ext/skia_utils_mac.h"
13 #include "ui/base/theme_provider.h"
14 #include "ui/native_theme/native_theme.h"
16 namespace {
17 const CGFloat kDegrees90               = (M_PI / 2);
18 const CGFloat kDegrees180              = (M_PI);
19 const CGFloat kDegrees270              = (3 * M_PI / 2);
20 const CGFloat kDegrees360              = (2 * M_PI);
21 const CGFloat kDesignWidth             = 28.0;
22 const CGFloat kArcRadius               = 12.5;
23 const CGFloat kArcDiameter             = kArcRadius * 2.0;
24 const CGFloat kArcLength               = 58.9;
25 const CGFloat kArcStrokeWidth          = 3.0;
26 const CGFloat kArcAnimationTime        = 1.333;
27 const CGFloat kArcStartAngle           = kDegrees180;
28 const CGFloat kArcEndAngle             = (kArcStartAngle + kDegrees270);
29 const CGFloat kRotationTime            = 1.56863;
30 NSString* const kSpinnerAnimationName  = @"SpinnerAnimationName";
31 NSString* const kRotationAnimationName = @"RotationAnimationName";
34 @interface SpinnerView () {
35   base::scoped_nsobject<CAAnimationGroup> spinnerAnimation_;
36   base::scoped_nsobject<CABasicAnimation> rotationAnimation_;
37   CAShapeLayer* shapeLayer_;  // Weak.
38   CALayer* rotationLayer_;  // Weak.
40 @end
43 @implementation SpinnerView
45 - (instancetype)initWithFrame:(NSRect)frame {
46   if (self = [super initWithFrame:frame]) {
47     [self setWantsLayer:YES];
48   }
49   return self;
52 - (void)dealloc {
53   [[NSNotificationCenter defaultCenter] removeObserver:self];
54   [super dealloc];
57 // Register/unregister for window miniaturization event notifications so that
58 // the spinner can stop animating if the window is minaturized
59 // (i.e. not visible).
60 - (void)viewWillMoveToWindow:(NSWindow*)newWindow {
61   if ([self window]) {
62     [[NSNotificationCenter defaultCenter]
63         removeObserver:self
64                   name:NSWindowWillMiniaturizeNotification
65                 object:[self window]];
66     [[NSNotificationCenter defaultCenter]
67         removeObserver:self
68                   name:NSWindowDidDeminiaturizeNotification
69                 object:[self window]];
70   }
72   if (newWindow) {
73     [[NSNotificationCenter defaultCenter]
74         addObserver:self
75            selector:@selector(updateAnimation:)
76                name:NSWindowWillMiniaturizeNotification
77              object:newWindow];
78     [[NSNotificationCenter defaultCenter]
79         addObserver:self
80            selector:@selector(updateAnimation:)
81                name:NSWindowDidDeminiaturizeNotification
82              object:newWindow];
83   }
86 // Start or stop the animation whenever the view is added to or removed from a
87 // window.
88 - (void)viewDidMoveToWindow {
89   [self updateAnimation:nil];
92 - (BOOL)isAnimating {
93   return [shapeLayer_ animationForKey:kSpinnerAnimationName] != nil;
96 // Overridden to return a custom CALayer for the view (called from
97 // setWantsLayer:).
98 - (CALayer*)makeBackingLayer {
99   CGRect bounds = [self bounds];
100   // The spinner was designed to be |kDesignWidth| points wide. Compute the
101   // scale factor needed to scale design parameters like |RADIUS| so that the
102   // spinner scales to fit the view's bounds.
103   CGFloat scaleFactor = bounds.size.width / kDesignWidth;
105   shapeLayer_ = [CAShapeLayer layer];
106   [shapeLayer_ setDelegate:self];
107   [shapeLayer_ setBounds:bounds];
108   // Per the design, the line width does not scale linearly.
109   CGFloat scaledDiameter = kArcDiameter * scaleFactor;
110   CGFloat lineWidth;
111   if (scaledDiameter < kArcDiameter) {
112     lineWidth = kArcStrokeWidth - (kArcDiameter - scaledDiameter) / 16.0;
113   } else {
114     lineWidth = kArcStrokeWidth + (scaledDiameter - kArcDiameter) / 11.0;
115   }
116   [shapeLayer_ setLineWidth:lineWidth];
117   [shapeLayer_ setLineCap:kCALineCapRound];
118   [shapeLayer_ setLineDashPattern:@[ @(kArcLength * scaleFactor) ]];
119   [shapeLayer_ setFillColor:NULL];
120   ui::NativeTheme* nativeTheme = ui::NativeTheme::instance();
121   SkColor throbberBlueColor = nativeTheme->GetSystemColor(
122       ui::NativeTheme::kColorId_ThrobberSpinningColor);
123   CGColorRef blueColor = gfx::CGColorCreateFromSkColor(throbberBlueColor);
124   [shapeLayer_ setStrokeColor:blueColor];
125   CGColorRelease(blueColor);
127   // Create the arc that, when stroked, creates the spinner.
128   base::ScopedCFTypeRef<CGMutablePathRef> shapePath(CGPathCreateMutable());
129   CGPathAddArc(shapePath, NULL, bounds.size.width / 2.0,
130                bounds.size.height / 2.0, kArcRadius * scaleFactor,
131                kArcStartAngle, kArcEndAngle, 0);
132   [shapeLayer_ setPath:shapePath];
134   // Place |shapeLayer_| in a layer so that it's easy to rotate the entire
135   // spinner animation.
136   rotationLayer_ = [CALayer layer];
137   [rotationLayer_ setBounds:bounds];
138   [rotationLayer_ addSublayer:shapeLayer_];
139   [shapeLayer_ setPosition:CGPointMake(NSMidX(bounds), NSMidY(bounds))];
141   // Place |rotationLayer_| in a parent layer so that it's easy to rotate
142   // |rotationLayer_| around the center of the view.
143   CALayer* parentLayer = [CALayer layer];
144   [parentLayer setBounds:bounds];
145   [parentLayer addSublayer:rotationLayer_];
146   [rotationLayer_ setPosition:CGPointMake(bounds.size.width / 2.0,
147                                           bounds.size.height / 2.0)];
148   return parentLayer;
151 // Overridden to start or stop the animation whenever the view is unhidden or
152 // hidden.
153 - (void)setHidden:(BOOL)flag {
154   [super setHidden:flag];
155   [self updateAnimation:nil];
158 // Make sure the layer's backing store matches the window as the window moves
159 // between screens.
160 - (BOOL)layer:(CALayer*)layer
161     shouldInheritContentsScale:(CGFloat)newScale
162                     fromWindow:(NSWindow*)window {
163   return YES;
166 // The spinner animation consists of four cycles that it continuously repeats.
167 // Each cycle consists of one complete rotation of the spinner's arc plus a
168 // rotation adjustment at the end of each cycle (see rotation animation comment
169 // below for the reason for the adjustment). The arc's length also grows and
170 // shrinks over the course of each cycle, which the spinner achieves by drawing
171 // the arc using a (solid) dashed line pattern and animating the "lineDashPhase"
172 // property.
173 - (void)initializeAnimation {
174   CGRect bounds = [self bounds];
175   CGFloat scaleFactor = bounds.size.width / kDesignWidth;
177   // Make sure |shapeLayer_|'s content scale factor matches the window's
178   // backing depth (e.g. it's 2.0 on Retina Macs). Don't worry about adjusting
179   // any other layers because |shapeLayer_| is the only one displaying content.
180   if (base::mac::IsOSLionOrLater()) {
181     CGFloat backingScaleFactor = [[self window] backingScaleFactor];
182     [shapeLayer_ setContentsScale:backingScaleFactor];
183   }
185   // Create the first half of the arc animation, where it grows from a short
186   // block to its full length.
187   base::scoped_nsobject<CAMediaTimingFunction> timingFunction(
188       [[CAMediaTimingFunction alloc] initWithControlPoints:0.4 :0.0 :0.2 :1]);
189   base::scoped_nsobject<CAKeyframeAnimation> firstHalfAnimation(
190       [[CAKeyframeAnimation alloc] init]);
191   [firstHalfAnimation setTimingFunction:timingFunction];
192   [firstHalfAnimation setKeyPath:@"lineDashPhase"];
193   // Begin the lineDashPhase animation just short of the full arc length,
194   // otherwise the arc will be zero length at start.
195   NSArray* animationValues = @[ @(-(kArcLength - 0.4) * scaleFactor), @(0.0) ];
196   [firstHalfAnimation setValues:animationValues];
197   NSArray* keyTimes = @[ @(0.0), @(1.0) ];
198   [firstHalfAnimation setKeyTimes:keyTimes];
199   [firstHalfAnimation setDuration:kArcAnimationTime / 2.0];
200   [firstHalfAnimation setRemovedOnCompletion:NO];
201   [firstHalfAnimation setFillMode:kCAFillModeForwards];
203   // Create the second half of the arc animation, where it shrinks from full
204   // length back to a short block.
205   base::scoped_nsobject<CAKeyframeAnimation> secondHalfAnimation(
206       [[CAKeyframeAnimation alloc] init]);
207   [secondHalfAnimation setTimingFunction:timingFunction];
208   [secondHalfAnimation setKeyPath:@"lineDashPhase"];
209   // Stop the lineDashPhase animation just before it reaches the full arc
210   // length, otherwise the arc will be zero length at the end.
211   animationValues = @[ @(0.0), @((kArcLength - 0.3) * scaleFactor) ];
212   [secondHalfAnimation setValues:animationValues];
213   [secondHalfAnimation setKeyTimes:keyTimes];
214   [secondHalfAnimation setDuration:kArcAnimationTime / 2.0];
215   [secondHalfAnimation setRemovedOnCompletion:NO];
216   [secondHalfAnimation setFillMode:kCAFillModeForwards];
218   // Make four copies of the arc animations, to cover the four complete cycles
219   // of the full animation.
220   NSMutableArray* animations = [NSMutableArray array];
221   CGFloat beginTime = 0;
222   for (NSUInteger i = 0; i < 4; i++, beginTime += kArcAnimationTime) {
223     [firstHalfAnimation setBeginTime:beginTime];
224     [secondHalfAnimation setBeginTime:beginTime + kArcAnimationTime / 2.0];
225     [animations addObject:firstHalfAnimation];
226     [animations addObject:secondHalfAnimation];
227     firstHalfAnimation.reset([firstHalfAnimation copy]);
228     secondHalfAnimation.reset([secondHalfAnimation copy]);
229   }
231   // Create a step rotation animation, which rotates the arc 90 degrees on each
232   // cycle. Each arc starts as a short block at degree 0 and ends as a short
233   // block at degree -270. Without a 90 degree rotation at the end of each
234   // cycle, the short block would appear to suddenly jump from -270 degrees to
235   // -360 degrees. The full animation has to contain four of these 90 degree
236   // adjustments in order for the arc to return to its starting point, at which
237   // point the full animation can smoothly repeat.
238   CAKeyframeAnimation* stepRotationAnimation = [CAKeyframeAnimation animation];
239   [stepRotationAnimation setTimingFunction:
240       [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
241   [stepRotationAnimation setKeyPath:@"transform.rotation"];
242   animationValues = @[ @(0.0), @(0.0),
243                        @(kDegrees90),
244                        @(kDegrees90),
245                        @(kDegrees180),
246                        @(kDegrees180),
247                        @(kDegrees270),
248                        @(kDegrees270)];
249   [stepRotationAnimation setValues:animationValues];
250   keyTimes = @[ @(0.0), @(0.25), @(0.25), @(0.5), @(0.5), @(0.75), @(0.75),
251                 @(1.0) ];
252   [stepRotationAnimation setKeyTimes:keyTimes];
253   [stepRotationAnimation setDuration:kArcAnimationTime * 4.0];
254   [stepRotationAnimation setRemovedOnCompletion:NO];
255   [stepRotationAnimation setFillMode:kCAFillModeForwards];
256   [stepRotationAnimation setRepeatCount:HUGE_VALF];
257   [animations addObject:stepRotationAnimation];
259   // Use an animation group so that the animations are easier to manage, and to
260   // give them the best chance of firing synchronously.
261   CAAnimationGroup* group = [CAAnimationGroup animation];
262   [group setDuration:kArcAnimationTime * 4];
263   [group setRepeatCount:HUGE_VALF];
264   [group setFillMode:kCAFillModeForwards];
265   [group setRemovedOnCompletion:NO];
266   [group setAnimations:animations];
268   spinnerAnimation_.reset([group retain]);
270   // Finally, create an animation that rotates the entire spinner layer.
271   CABasicAnimation* rotationAnimation = [CABasicAnimation animation];
272   rotationAnimation.keyPath = @"transform.rotation";
273   [rotationAnimation setFromValue:@0];
274   [rotationAnimation setToValue:@(-kDegrees360)];
275   [rotationAnimation setDuration:kRotationTime];
276   [rotationAnimation setRemovedOnCompletion:NO];
277   [rotationAnimation setFillMode:kCAFillModeForwards];
278   [rotationAnimation setRepeatCount:HUGE_VALF];
280   rotationAnimation_.reset([rotationAnimation retain]);
283 - (void)updateAnimation:(NSNotification*)notification {
284   // Only animate the spinner if it's within a window, and that window is not
285   // currently minimized or being minimized.
286   if ([self window] && ![[self window] isMiniaturized] && ![self isHidden] &&
287       ![[notification name] isEqualToString:
288            NSWindowWillMiniaturizeNotification]) {
289     if (spinnerAnimation_.get() == nil) {
290       [self initializeAnimation];
291     }
292     if (![self isAnimating]) {
293       [shapeLayer_ addAnimation:spinnerAnimation_.get()
294                          forKey:kSpinnerAnimationName];
295       [rotationLayer_ addAnimation:rotationAnimation_.get()
296                             forKey:kRotationAnimationName];
297     }
298   } else {
299     [shapeLayer_ removeAllAnimations];
300     [rotationLayer_ removeAllAnimations];
301   }
304 @end