1// Copyright 2014 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.
4
5#import <QuartzCore/QuartzCore.h>
6
7#include "content/browser/web_contents/web_contents_view_overscroll_animator_slider_mac.h"
8
9#include "content/browser/web_contents/web_contents_impl.h"
10#include "content/public/browser/web_contents_observer.h"
11
12namespace {
13// The minimum possible progress of an overscroll animation.
14CGFloat kMinProgress = 0;
15// The maximum possible progress of an overscroll animation.
16CGFloat kMaxProgress = 2.0;
17// The maximum duration of the completion or cancellation animations. The
18// effective maximum is half of this value, since the longest animation is from
19// progress = 1.0 to progress = 2.0;
20CGFloat kMaxAnimationDuration = 0.2;
21}  // namespace
22
23// OverscrollAnimatorSliderView Private Category -------------------------------
24
25@interface OverscrollAnimatorSliderView ()
26// Callback from WebContentsPaintObserver.
27- (void)webContentsFinishedNonEmptyPaint;
28
29// Resets overscroll animation state.
30- (void)reset;
31
32// Given a |progress| from 0 to 2, the expected frame origin of the -movingView.
33- (NSPoint)frameOriginWithProgress:(CGFloat)progress;
34
35// The NSView that is moving during the overscroll animation.
36- (NSView*)movingView;
37
38// The expected duration of an animation from progress_ to |progress|
39- (CGFloat)animationDurationForProgress:(CGFloat)progress;
40
41// NSView override. During an overscroll animation, the cursor may no longer
42// rest on the RenderWidgetHost's NativeView, which prevents wheel events from
43// reaching the NativeView. The overscroll animation is driven by wheel events
44// so they must be explicitly forwarded to the NativeView.
45- (void)scrollWheel:(NSEvent*)event;
46@end
47
48// Helper Class (ResizingView) -------------------------------------------------
49
50// This NSView subclass is intended to be the RenderWidgetHost's NativeView's
51// parent NSView. It is possible for the RenderWidgetHost's NativeView's size to
52// become out of sync with its parent NSView. The override of
53// -resizeSubviewsWithOldSize: ensures that the sizes will eventually become
54// consistent.
55// http://crbug.com/264207
56@interface ResizingView : NSView
57@end
58
59@implementation ResizingView
60- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
61  for (NSView* subview in self.subviews)
62    [subview setFrame:self.bounds];
63}
64@end
65
66// Helper Class (WebContentsPaintObserver) -------------------------------------
67
68namespace overscroll_animator {
69class WebContentsPaintObserver : public content::WebContentsObserver {
70 public:
71  WebContentsPaintObserver(content::WebContents* web_contents,
72                           OverscrollAnimatorSliderView* slider_view)
73      : WebContentsObserver(web_contents), slider_view_(slider_view) {}
74
75  virtual void DidFirstVisuallyNonEmptyPaint() OVERRIDE {
76    [slider_view_ webContentsFinishedNonEmptyPaint];
77  }
78
79 private:
80  OverscrollAnimatorSliderView* slider_view_;  // Weak reference.
81};
82}  // namespace overscroll_animator
83
84// OverscrollAnimatorSliderView Implementation ---------------------------------
85
86@implementation OverscrollAnimatorSliderView
87
88- (instancetype)initWithFrame:(NSRect)frame {
89  self = [super initWithFrame:frame];
90  if (self) {
91    bottomView_.reset([[NSImageView alloc] initWithFrame:self.bounds]);
92    bottomView_.get().imageScaling = NSImageScaleNone;
93    bottomView_.get().autoresizingMask =
94        NSViewWidthSizable | NSViewHeightSizable;
95    bottomView_.get().imageAlignment = NSImageAlignTop;
96    [self addSubview:bottomView_];
97    middleView_.reset([[ResizingView alloc] initWithFrame:self.bounds]);
98    middleView_.get().autoresizingMask =
99        NSViewWidthSizable | NSViewHeightSizable;
100    [self addSubview:middleView_];
101    topView_.reset([[NSImageView alloc] initWithFrame:self.bounds]);
102    topView_.get().autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
103    topView_.get().imageScaling = NSImageScaleNone;
104    topView_.get().imageAlignment = NSImageAlignTop;
105    [self addSubview:topView_];
106
107    [self reset];
108  }
109  return self;
110}
111
112- (void)webContentsFinishedNonEmptyPaint {
113  observer_.reset();
114  [self reset];
115}
116
117- (void)reset {
118  DCHECK(!animating_);
119  inOverscroll_ = NO;
120  progress_ = kMinProgress;
121
122  [CATransaction begin];
123  [CATransaction setDisableActions:YES];
124  bottomView_.get().hidden = YES;
125  middleView_.get().hidden = NO;
126  topView_.get().hidden = YES;
127
128  [bottomView_ setFrameOrigin:NSMakePoint(0, 0)];
129  [middleView_ setFrameOrigin:NSMakePoint(0, 0)];
130  [topView_ setFrameOrigin:NSMakePoint(0, 0)];
131  [CATransaction commit];
132}
133
134- (NSPoint)frameOriginWithProgress:(CGFloat)progress {
135  if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
136    return NSMakePoint(progress / kMaxProgress * self.bounds.size.width, 0);
137  return NSMakePoint((1 - progress / kMaxProgress) * self.bounds.size.width, 0);
138}
139
140- (NSView*)movingView {
141  if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
142    return middleView_;
143  return topView_;
144}
145
146- (CGFloat)animationDurationForProgress:(CGFloat)progress {
147  CGFloat progressPercentage =
148      fabs(progress_ - progress) / (kMaxProgress - kMinProgress);
149  return progressPercentage * kMaxAnimationDuration;
150}
151
152- (void)scrollWheel:(NSEvent*)event {
153  NSView* latestRenderWidgetHostView = [[middleView_ subviews] lastObject];
154  [latestRenderWidgetHostView scrollWheel:event];
155}
156
157// WebContentsOverscrollAnimator Implementation --------------------------------
158
159- (BOOL)needsNavigationSnapshot {
160  return YES;
161}
162
163- (void)beginOverscrollInDirection:
164            (content::OverscrollAnimatorDirection)direction
165                navigationSnapshot:(NSImage*)snapshot {
166  // TODO(erikchen): If snapshot is nil, need a placeholder.
167  if (animating_ || inOverscroll_)
168    return;
169
170  inOverscroll_ = YES;
171  direction_ = direction;
172  if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS) {
173    // The middleView_ will slide to the right, revealing bottomView_.
174    bottomView_.get().hidden = NO;
175    [bottomView_ setImage:snapshot];
176  } else {
177    // The topView_ will slide in from the right, concealing middleView_.
178    topView_.get().hidden = NO;
179    [topView_ setFrameOrigin:NSMakePoint(self.bounds.size.width, 0)];
180    [topView_ setImage:snapshot];
181  }
182
183  [self updateOverscrollProgress:kMinProgress];
184}
185
186- (void)addRenderWidgetHostNativeView:(NSView*)view {
187  [middleView_ addSubview:view];
188}
189
190- (void)updateOverscrollProgress:(CGFloat)progress {
191  if (animating_)
192    return;
193  DCHECK_LE(progress, kMaxProgress);
194  DCHECK_GE(progress, kMinProgress);
195  progress_ = progress;
196  [[self movingView] setFrameOrigin:[self frameOriginWithProgress:progress]];
197}
198
199- (void)completeOverscroll:(content::WebContentsImpl*)webContents {
200  if (animating_ || !inOverscroll_)
201    return;
202
203  animating_ = YES;
204
205  NSView* view = [self movingView];
206  [NSAnimationContext beginGrouping];
207  [NSAnimationContext currentContext].duration =
208      [self animationDurationForProgress:kMaxProgress];
209  [[NSAnimationContext currentContext] setCompletionHandler:^{
210      animating_ = NO;
211
212      // Animation is complete. Now perform page load.
213      if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
214        webContents->GetController().GoBack();
215      else
216        webContents->GetController().GoForward();
217
218      // Reset the position of the middleView_, but wait for the page to paint
219      // before showing it.
220      middleView_.get().hidden = YES;
221      [middleView_ setFrameOrigin:NSMakePoint(0, 0)];
222      observer_.reset(
223          new overscroll_animator::WebContentsPaintObserver(webContents, self));
224  }];
225
226  // Animate the moving view to its final position.
227  [[view animator] setFrameOrigin:[self frameOriginWithProgress:kMaxProgress]];
228
229  [NSAnimationContext endGrouping];
230}
231
232- (void)cancelOverscroll {
233  if (animating_)
234    return;
235
236  if (!inOverscroll_) {
237    [self reset];
238    return;
239  }
240
241  animating_ = YES;
242
243  NSView* view = [self movingView];
244  [NSAnimationContext beginGrouping];
245  [NSAnimationContext currentContext].duration =
246      [self animationDurationForProgress:kMinProgress];
247  [[NSAnimationContext currentContext] setCompletionHandler:^{
248      // Animation is complete. Reset the state.
249      animating_ = NO;
250      [self reset];
251  }];
252
253  // Animate the moving view to its initial position.
254  [[view animator] setFrameOrigin:[self frameOriginWithProgress:kMinProgress]];
255
256  [NSAnimationContext endGrouping];
257}
258
259@end
260