1/*
2 * Copyright (C) 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#include "third_party/blink/renderer/core/scroll/scroll_animator_mac.h"
27
28#import <AppKit/AppKit.h>
29
30#include <memory>
31#include "base/mac/scoped_cftyperef.h"
32#include "base/memory/scoped_policy.h"
33#include "third_party/blink/public/platform/platform.h"
34#include "third_party/blink/renderer/core/scroll/ns_scroller_imp_details.h"
35#include "third_party/blink/renderer/core/scroll/scrollable_area.h"
36#include "third_party/blink/renderer/core/scroll/scrollbar_theme.h"
37#include "third_party/blink/renderer/core/scroll/scrollbar_theme_mac.h"
38#include "third_party/blink/renderer/platform/animation/timing_function.h"
39#include "third_party/blink/renderer/platform/geometry/float_rect.h"
40#include "third_party/blink/renderer/platform/geometry/int_rect.h"
41#include "third_party/blink/renderer/platform/mac/block_exceptions.h"
42#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
43#include "third_party/blink/renderer/platform/timer.h"
44#include "third_party/blink/renderer/platform/wtf/math_extras.h"
45
46namespace {
47
48bool SupportsUIStateTransitionProgress() {
49  // FIXME: This is temporary until all platforms that support ScrollbarPainter
50  // support this part of the API.
51  static bool global_supports_ui_state_transition_progress =
52      [NSClassFromString(@"NSScrollerImp")
53          instancesRespondToSelector:@selector(mouseEnteredScroller)];
54  return global_supports_ui_state_transition_progress;
55}
56
57bool SupportsExpansionTransitionProgress() {
58  static bool global_supports_expansion_transition_progress =
59      [NSClassFromString(@"NSScrollerImp")
60          instancesRespondToSelector:@selector(expansionTransitionProgress)];
61  return global_supports_expansion_transition_progress;
62}
63
64bool SupportsContentAreaScrolledInDirection() {
65  static bool global_supports_content_area_scrolled_in_direction =
66      [NSClassFromString(@"NSScrollerImpPair")
67          instancesRespondToSelector:@selector
68          (contentAreaScrolledInDirection:)];
69  return global_supports_content_area_scrolled_in_direction;
70}
71
72blink::ScrollbarThemeMac* MacOverlayScrollbarTheme(
73    blink::ScrollbarTheme& scrollbar_theme) {
74  return !scrollbar_theme.IsMockTheme()
75             ? static_cast<blink::ScrollbarThemeMac*>(&scrollbar_theme)
76             : nil;
77}
78
79ScrollbarPainter ScrollbarPainterForScrollbar(blink::Scrollbar& scrollbar) {
80  if (blink::ScrollbarThemeMac* scrollbar_theme =
81          MacOverlayScrollbarTheme(scrollbar.GetTheme()))
82    return scrollbar_theme->PainterForScrollbar(scrollbar);
83
84  return nil;
85}
86
87}  // namespace
88
89@interface NSObject (ScrollAnimationHelperDetails)
90- (id)initWithDelegate:(id)delegate;
91- (void)_stopRun;
92- (BOOL)_isAnimating;
93- (NSPoint)targetOrigin;
94- (CGFloat)_progress;
95@end
96
97@interface BlinkScrollAnimationHelperDelegate : NSObject {
98  blink::ScrollAnimatorMac* _animator;
99}
100- (id)initWithScrollAnimator:(blink::ScrollAnimatorMac*)scrollAnimator;
101@end
102
103static NSSize abs(NSSize size) {
104  NSSize finalSize = size;
105  if (finalSize.width < 0)
106    finalSize.width = -finalSize.width;
107  if (finalSize.height < 0)
108    finalSize.height = -finalSize.height;
109  return finalSize;
110}
111
112@implementation BlinkScrollAnimationHelperDelegate
113
114- (id)initWithScrollAnimator:(blink::ScrollAnimatorMac*)scrollAnimator {
115  self = [super init];
116  if (!self)
117    return nil;
118
119  _animator = scrollAnimator;
120  return self;
121}
122
123- (void)invalidate {
124  _animator = 0;
125}
126
127- (NSRect)bounds {
128  if (!_animator)
129    return NSZeroRect;
130
131  blink::ScrollOffset currentOffset = _animator->CurrentOffset();
132  return NSMakeRect(currentOffset.Width(), currentOffset.Height(), 0, 0);
133}
134
135- (void)_immediateScrollToPoint:(NSPoint)newPosition {
136  if (!_animator)
137    return;
138  _animator->ImmediateScrollToOffsetForScrollAnimation(
139      blink::ToScrollOffset(newPosition));
140}
141
142- (NSPoint)_pixelAlignProposedScrollPosition:(NSPoint)newOrigin {
143  return newOrigin;
144}
145
146- (NSSize)convertSizeToBase:(NSSize)size {
147  return abs(size);
148}
149
150- (NSSize)convertSizeFromBase:(NSSize)size {
151  return abs(size);
152}
153
154- (NSSize)convertSizeToBacking:(NSSize)size {
155  return abs(size);
156}
157
158- (NSSize)convertSizeFromBacking:(NSSize)size {
159  return abs(size);
160}
161
162- (id)superview {
163  return nil;
164}
165
166- (id)documentView {
167  return nil;
168}
169
170- (id)window {
171  return nil;
172}
173
174- (void)_recursiveRecomputeToolTips {
175}
176
177@end
178
179@interface BlinkScrollbarPainterControllerDelegate : NSObject {
180  blink::ScrollableArea* _scrollableArea;
181}
182- (id)initWithScrollableArea:(blink::ScrollableArea*)scrollableArea;
183@end
184
185@implementation BlinkScrollbarPainterControllerDelegate
186
187- (id)initWithScrollableArea:(blink::ScrollableArea*)scrollableArea {
188  self = [super init];
189  if (!self)
190    return nil;
191
192  _scrollableArea = scrollableArea;
193  return self;
194}
195
196- (void)invalidate {
197  _scrollableArea = 0;
198}
199
200- (NSRect)contentAreaRectForScrollerImpPair:(id)scrollerImpPair {
201  if (!_scrollableArea)
202    return NSZeroRect;
203
204  blink::IntSize contentsSize = _scrollableArea->ContentsSize();
205  return NSMakeRect(0, 0, contentsSize.Width(), contentsSize.Height());
206}
207
208- (BOOL)inLiveResizeForScrollerImpPair:(id)scrollerImpPair {
209  return NO;
210}
211
212- (NSPoint)mouseLocationInContentAreaForScrollerImpPair:(id)scrollerImpPair {
213  if (!_scrollableArea)
214    return NSZeroPoint;
215
216  return _scrollableArea->LastKnownMousePosition();
217}
218
219- (NSPoint)scrollerImpPair:(id)scrollerImpPair
220       convertContentPoint:(NSPoint)pointInContentArea
221             toScrollerImp:(id)scrollerImp {
222  if (!_scrollableArea || !scrollerImp)
223    return NSZeroPoint;
224
225  blink::Scrollbar* scrollbar = nil;
226  if ([scrollerImp isHorizontal])
227    scrollbar = _scrollableArea->HorizontalScrollbar();
228  else
229    scrollbar = _scrollableArea->VerticalScrollbar();
230
231  // It is possible to have a null scrollbar here since it is possible for this
232  // delegate method to be called between the moment when a scrollbar has been
233  // set to 0 and the moment when its destructor has been called.
234  if (!scrollbar)
235    return NSZeroPoint;
236
237  DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*scrollbar));
238
239  return scrollbar->ConvertFromContainingEmbeddedContentView(
240      blink::IntPoint(pointInContentArea));
241}
242
243- (void)scrollerImpPair:(id)scrollerImpPair
244    setContentAreaNeedsDisplayInRect:(NSRect)rect {
245  if (!_scrollableArea)
246    return;
247
248  if (!_scrollableArea->ScrollbarsCanBeActive())
249    return;
250
251  _scrollableArea->GetScrollAnimator().ContentAreaWillPaint();
252}
253
254- (void)scrollerImpPair:(id)scrollerImpPair
255    updateScrollerStyleForNewRecommendedScrollerStyle:
256        (NSScrollerStyle)newRecommendedScrollerStyle {
257  // Chrome has a single process mode which is used for testing on Mac. In that
258  // mode, WebKit runs on a thread in the
259  // browser process. This notification is called by the OS on the main thread
260  // in the browser process, and not on the
261  // the WebKit thread. Better to not update the style than crash.
262  // http://crbug.com/126514
263  if (!IsMainThread())
264    return;
265
266  if (!_scrollableArea)
267    return;
268
269  [scrollerImpPair setScrollerStyle:newRecommendedScrollerStyle];
270
271  static_cast<blink::ScrollAnimatorMac&>(_scrollableArea->GetScrollAnimator())
272      .UpdateScrollerStyle();
273}
274
275@end
276
277enum FeatureToAnimate {
278  ThumbAlpha,
279  TrackAlpha,
280  UIStateTransition,
281  ExpansionTransition
282};
283
284@class BlinkScrollbarPartAnimation;
285
286namespace blink {
287
288// This class is used to drive the animation timer for
289// BlinkScrollbarPartAnimation
290// objects. This is used instead of NSAnimation because CoreAnimation
291// establishes connections to the WindowServer, which should not be done in a
292// sandboxed renderer process.
293class BlinkScrollbarPartAnimationTimer {
294 public:
295  BlinkScrollbarPartAnimationTimer(BlinkScrollbarPartAnimation* animation,
296                                   CFTimeInterval duration)
297      : timer_(ThreadScheduler::Current()->CompositorTaskRunner(),
298               this,
299               &BlinkScrollbarPartAnimationTimer::TimerFired),
300        start_time_(0.0),
301        duration_(duration),
302        animation_(animation),
303        timing_function_(CubicBezierTimingFunction::Preset(
304            CubicBezierTimingFunction::EaseType::EASE_IN_OUT)) {}
305
306  ~BlinkScrollbarPartAnimationTimer() {}
307
308  void Start() {
309    start_time_ = base::Time::Now().ToDoubleT();
310    // Set the framerate of the animation. NSAnimation uses a default
311    // framerate of 60 Hz, so use that here.
312    timer_.StartRepeating(base::TimeDelta::FromSecondsD(1.0 / 60.0), FROM_HERE);
313  }
314
315  void Stop() { timer_.Stop(); }
316
317  void SetDuration(CFTimeInterval duration) { duration_ = duration; }
318
319 private:
320  void TimerFired(TimerBase*) {
321    double current_time = base::Time::Now().ToDoubleT();
322    double delta = current_time - start_time_;
323
324    if (delta >= duration_)
325      timer_.Stop();
326
327    double fraction = delta / duration_;
328    fraction = clampTo(fraction, 0.0, 1.0);
329    double progress = timing_function_->Evaluate(fraction);
330    [animation_ setCurrentProgress:progress];
331  }
332
333  TaskRunnerTimer<BlinkScrollbarPartAnimationTimer> timer_;
334  double start_time_;                       // In seconds.
335  double duration_;                         // In seconds.
336  BlinkScrollbarPartAnimation* animation_;  // Weak, owns this.
337  scoped_refptr<CubicBezierTimingFunction> timing_function_;
338};
339
340}  // namespace blink
341
342@interface BlinkScrollbarPartAnimation : NSObject {
343  blink::Scrollbar* _scrollbar;
344  std::unique_ptr<blink::BlinkScrollbarPartAnimationTimer> _timer;
345  base::scoped_nsobject<ScrollbarPainter> _scrollbarPainter;
346  FeatureToAnimate _featureToAnimate;
347  CGFloat _startValue;
348  CGFloat _endValue;
349}
350- (id)initWithScrollbar:(blink::Scrollbar*)scrollbar
351       featureToAnimate:(FeatureToAnimate)featureToAnimate
352            animateFrom:(CGFloat)startValue
353              animateTo:(CGFloat)endValue
354               duration:(NSTimeInterval)duration;
355@end
356
357@implementation BlinkScrollbarPartAnimation
358
359- (id)initWithScrollbar:(blink::Scrollbar*)scrollbar
360       featureToAnimate:(FeatureToAnimate)featureToAnimate
361            animateFrom:(CGFloat)startValue
362              animateTo:(CGFloat)endValue
363               duration:(NSTimeInterval)duration {
364  self = [super init];
365  if (!self)
366    return nil;
367
368  _timer =
369      std::make_unique<blink::BlinkScrollbarPartAnimationTimer>(self, duration);
370  _scrollbar = scrollbar;
371  _featureToAnimate = featureToAnimate;
372  _startValue = startValue;
373  _endValue = endValue;
374
375  return self;
376}
377
378- (void)startAnimation {
379  DCHECK(_scrollbar);
380
381  _scrollbarPainter.reset(ScrollbarPainterForScrollbar(*_scrollbar),
382                          base::scoped_policy::RETAIN);
383  _timer->Start();
384}
385
386- (void)stopAnimation {
387  _timer->Stop();
388}
389
390- (void)setDuration:(CFTimeInterval)duration {
391  _timer->SetDuration(duration);
392}
393
394- (void)setStartValue:(CGFloat)startValue {
395  _startValue = startValue;
396}
397
398- (void)setEndValue:(CGFloat)endValue {
399  _endValue = endValue;
400}
401
402- (void)setCurrentProgress:(NSAnimationProgress)progress {
403  DCHECK(_scrollbar);
404
405  CGFloat currentValue;
406  if (_startValue > _endValue)
407    currentValue = 1 - progress;
408  else
409    currentValue = progress;
410
411  blink::ScrollbarPart invalidParts = blink::kNoPart;
412  switch (_featureToAnimate) {
413    case ThumbAlpha:
414      [_scrollbarPainter setKnobAlpha:currentValue];
415      break;
416    case TrackAlpha:
417      [_scrollbarPainter setTrackAlpha:currentValue];
418      invalidParts = static_cast<blink::ScrollbarPart>(~blink::kThumbPart);
419      break;
420    case UIStateTransition:
421      [_scrollbarPainter setUiStateTransitionProgress:currentValue];
422      invalidParts = blink::kAllParts;
423      break;
424    case ExpansionTransition:
425      [_scrollbarPainter setExpansionTransitionProgress:currentValue];
426      invalidParts = blink::kThumbPart;
427      break;
428  }
429
430  _scrollbar->SetNeedsPaintInvalidation(invalidParts);
431}
432
433- (void)invalidate {
434  BEGIN_BLOCK_OBJC_EXCEPTIONS;
435  [self stopAnimation];
436  END_BLOCK_OBJC_EXCEPTIONS;
437  _scrollbar = 0;
438}
439
440@end
441
442@interface BlinkScrollbarPainterDelegate : NSObject<NSAnimationDelegate> {
443  blink::Scrollbar* _scrollbar;
444
445  base::scoped_nsobject<BlinkScrollbarPartAnimation> _knobAlphaAnimation;
446  base::scoped_nsobject<BlinkScrollbarPartAnimation> _trackAlphaAnimation;
447  base::scoped_nsobject<BlinkScrollbarPartAnimation>
448      _uiStateTransitionAnimation;
449  base::scoped_nsobject<BlinkScrollbarPartAnimation>
450      _expansionTransitionAnimation;
451  BOOL _hasExpandedSinceInvisible;
452}
453- (id)initWithScrollbar:(blink::Scrollbar*)scrollbar;
454- (void)updateVisibilityImmediately:(bool)show;
455- (void)cancelAnimations;
456@end
457
458@implementation BlinkScrollbarPainterDelegate
459
460- (id)initWithScrollbar:(blink::Scrollbar*)scrollbar {
461  self = [super init];
462  if (!self)
463    return nil;
464
465  _scrollbar = scrollbar;
466  return self;
467}
468
469- (void)updateVisibilityImmediately:(bool)show {
470  [self cancelAnimations];
471  [ScrollbarPainterForScrollbar(*_scrollbar) setKnobAlpha:(show ? 1.0 : 0.0)];
472}
473
474- (void)cancelAnimations {
475  BEGIN_BLOCK_OBJC_EXCEPTIONS;
476  [_knobAlphaAnimation stopAnimation];
477  [_trackAlphaAnimation stopAnimation];
478  [_uiStateTransitionAnimation stopAnimation];
479  [_expansionTransitionAnimation stopAnimation];
480  END_BLOCK_OBJC_EXCEPTIONS;
481}
482
483- (blink::ScrollAnimatorMac&)scrollAnimator {
484  return static_cast<blink::ScrollAnimatorMac&>(
485      _scrollbar->GetScrollableArea()->GetScrollAnimator());
486}
487
488- (NSRect)convertRectToBacking:(NSRect)aRect {
489  return aRect;
490}
491
492- (NSRect)convertRectFromBacking:(NSRect)aRect {
493  return aRect;
494}
495
496- (NSPoint)mouseLocationInScrollerForScrollerImp:(id)scrollerImp {
497  if (!_scrollbar)
498    return NSZeroPoint;
499
500  DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar));
501
502  return _scrollbar->ConvertFromContainingEmbeddedContentView(
503      _scrollbar->GetScrollableArea()->LastKnownMousePosition());
504}
505
506- (void)setUpAlphaAnimation:
507            (base::scoped_nsobject<BlinkScrollbarPartAnimation>&)
508                scrollbarPartAnimation
509            scrollerPainter:(ScrollbarPainter)scrollerPainter
510                       part:(blink::ScrollbarPart)part
511             animateAlphaTo:(CGFloat)newAlpha
512                   duration:(NSTimeInterval)duration {
513  // If the user has scrolled the page, then the scrollbars must be animated
514  // here.
515  // This overrides the early returns.
516  bool mustAnimate = [self scrollAnimator].HaveScrolledSincePageLoad();
517
518  if ([self scrollAnimator].ScrollbarPaintTimerIsActive() && !mustAnimate)
519    return;
520
521  if (_scrollbar->GetScrollableArea()->ShouldSuspendScrollAnimations() &&
522      !mustAnimate) {
523    [self scrollAnimator].StartScrollbarPaintTimer();
524    return;
525  }
526
527  // At this point, we are definitely going to animate now, so stop the timer.
528  [self scrollAnimator].StopScrollbarPaintTimer();
529
530  // If we are currently animating, stop
531  if (scrollbarPartAnimation) {
532    [scrollbarPartAnimation stopAnimation];
533    scrollbarPartAnimation.reset();
534  }
535
536  scrollbarPartAnimation.reset([[BlinkScrollbarPartAnimation alloc]
537      initWithScrollbar:_scrollbar
538       featureToAnimate:part == blink::kThumbPart ? ThumbAlpha : TrackAlpha
539            animateFrom:part == blink::kThumbPart ? [scrollerPainter knobAlpha]
540                                                  : [scrollerPainter trackAlpha]
541              animateTo:newAlpha
542               duration:duration]);
543  [scrollbarPartAnimation startAnimation];
544}
545
546- (void)scrollerImp:(id)scrollerImp
547    animateKnobAlphaTo:(CGFloat)newKnobAlpha
548              duration:(NSTimeInterval)duration {
549  if (!_scrollbar)
550    return;
551
552  DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar));
553
554  ScrollbarPainter scrollerPainter = (ScrollbarPainter)scrollerImp;
555  [self setUpAlphaAnimation:_knobAlphaAnimation
556            scrollerPainter:scrollerPainter
557                       part:blink::kThumbPart
558             animateAlphaTo:newKnobAlpha
559                   duration:duration];
560}
561
562- (void)scrollerImp:(id)scrollerImp
563    animateTrackAlphaTo:(CGFloat)newTrackAlpha
564               duration:(NSTimeInterval)duration {
565  if (!_scrollbar)
566    return;
567
568  DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar));
569
570  ScrollbarPainter scrollerPainter = (ScrollbarPainter)scrollerImp;
571  [self setUpAlphaAnimation:_trackAlphaAnimation
572            scrollerPainter:scrollerPainter
573                       part:blink::kBackTrackPart
574             animateAlphaTo:newTrackAlpha
575                   duration:duration];
576}
577
578- (void)scrollerImp:(id)scrollerImp
579    animateUIStateTransitionWithDuration:(NSTimeInterval)duration {
580  if (!_scrollbar)
581    return;
582
583  if (!SupportsUIStateTransitionProgress())
584    return;
585
586  DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar));
587
588  ScrollbarPainter scrollbarPainter = (ScrollbarPainter)scrollerImp;
589
590  // UIStateTransition always animates to 1. In case an animation is in progress
591  // this avoids a hard transition.
592  [scrollbarPainter
593      setUiStateTransitionProgress:1 - [scrollerImp uiStateTransitionProgress]];
594
595  if (!_uiStateTransitionAnimation)
596    _uiStateTransitionAnimation.reset([[BlinkScrollbarPartAnimation alloc]
597        initWithScrollbar:_scrollbar
598         featureToAnimate:UIStateTransition
599              animateFrom:[scrollbarPainter uiStateTransitionProgress]
600                animateTo:1.0
601                 duration:duration]);
602  else {
603    // If we don't need to initialize the animation, just reset the values in
604    // case they have changed.
605    [_uiStateTransitionAnimation
606        setStartValue:[scrollbarPainter uiStateTransitionProgress]];
607    [_uiStateTransitionAnimation setEndValue:1.0];
608    [_uiStateTransitionAnimation setDuration:duration];
609  }
610  [_uiStateTransitionAnimation startAnimation];
611}
612
613- (void)scrollerImp:(id)scrollerImp
614    animateExpansionTransitionWithDuration:(NSTimeInterval)duration {
615  if (!_scrollbar)
616    return;
617
618  if (!SupportsExpansionTransitionProgress())
619    return;
620
621  DCHECK_EQ(scrollerImp, ScrollbarPainterForScrollbar(*_scrollbar));
622
623  ScrollbarPainter scrollbarPainter = (ScrollbarPainter)scrollerImp;
624
625  // ExpansionTransition always animates to 1. In case an animation is in
626  // progress this avoids a hard transition.
627  [scrollbarPainter
628      setExpansionTransitionProgress:1 -
629                                     [scrollerImp expansionTransitionProgress]];
630
631  if (!_expansionTransitionAnimation) {
632    _expansionTransitionAnimation.reset([[BlinkScrollbarPartAnimation alloc]
633        initWithScrollbar:_scrollbar
634         featureToAnimate:ExpansionTransition
635              animateFrom:[scrollbarPainter expansionTransitionProgress]
636                animateTo:1.0
637                 duration:duration]);
638  } else {
639    // If we don't need to initialize the animation, just reset the values in
640    // case they have changed.
641    [_expansionTransitionAnimation
642        setStartValue:[scrollbarPainter uiStateTransitionProgress]];
643    [_expansionTransitionAnimation setEndValue:1.0];
644    [_expansionTransitionAnimation setDuration:duration];
645  }
646  [_expansionTransitionAnimation startAnimation];
647}
648
649- (void)scrollerImp:(id)scrollerImp
650    overlayScrollerStateChangedTo:(NSUInteger)newOverlayScrollerState {
651  // The names of these states are based on their observed behavior, and are not
652  // based on documentation.
653  enum {
654    NSScrollerStateInvisible = 0,
655    NSScrollerStateKnob = 1,
656    NSScrollerStateExpanded = 2
657  };
658  // We do not receive notifications about the thumb un-expanding when the
659  // scrollbar fades away. Ensure
660  // that we re-paint the thumb the next time that we transition away from being
661  // invisible, so that
662  // the thumb doesn't stick in an expanded state.
663  if (newOverlayScrollerState == NSScrollerStateExpanded) {
664    _hasExpandedSinceInvisible = YES;
665  } else if (newOverlayScrollerState != NSScrollerStateInvisible &&
666             _hasExpandedSinceInvisible) {
667    _scrollbar->SetNeedsPaintInvalidation(blink::kThumbPart);
668    _hasExpandedSinceInvisible = NO;
669  }
670}
671
672- (void)invalidate {
673  _scrollbar = 0;
674  BEGIN_BLOCK_OBJC_EXCEPTIONS;
675  [_knobAlphaAnimation invalidate];
676  [_trackAlphaAnimation invalidate];
677  [_uiStateTransitionAnimation invalidate];
678  [_expansionTransitionAnimation invalidate];
679  END_BLOCK_OBJC_EXCEPTIONS;
680}
681
682@end
683
684namespace blink {
685
686ScrollAnimatorBase* ScrollAnimatorBase::Create(
687    blink::ScrollableArea* scrollable_area) {
688  return MakeGarbageCollected<ScrollAnimatorMac>(scrollable_area);
689}
690
691ScrollAnimatorMac::ScrollAnimatorMac(blink::ScrollableArea* scrollable_area)
692    : ScrollAnimatorBase(scrollable_area),
693      task_runner_(ThreadScheduler::Current()->CompositorTaskRunner()),
694      have_scrolled_since_page_load_(false),
695      needs_scroller_style_update_(false) {
696  scroll_animation_helper_delegate_.reset(
697      [[BlinkScrollAnimationHelperDelegate alloc] initWithScrollAnimator:this]);
698  scroll_animation_helper_.reset([[NSClassFromString(@"NSScrollAnimationHelper")
699      alloc] initWithDelegate:scroll_animation_helper_delegate_]);
700
701  scrollbar_painter_controller_delegate_.reset(
702      [[BlinkScrollbarPainterControllerDelegate alloc]
703          initWithScrollableArea:scrollable_area]);
704  scrollbar_painter_controller_.reset(
705      [[[NSClassFromString(@"NSScrollerImpPair") alloc] init] autorelease],
706      base::scoped_policy::RETAIN);
707  [scrollbar_painter_controller_
708      performSelector:@selector(setDelegate:)
709           withObject:scrollbar_painter_controller_delegate_];
710  [scrollbar_painter_controller_
711      setScrollerStyle:ScrollbarThemeMac::RecommendedScrollerStyle()];
712}
713
714ScrollAnimatorMac::~ScrollAnimatorMac() {}
715
716void ScrollAnimatorMac::Dispose() {
717  BEGIN_BLOCK_OBJC_EXCEPTIONS;
718  ScrollbarPainter horizontal_scrollbar_painter =
719      [scrollbar_painter_controller_ horizontalScrollerImp];
720  [horizontal_scrollbar_painter setDelegate:nil];
721
722  ScrollbarPainter vertical_scrollbar_painter =
723      [scrollbar_painter_controller_ verticalScrollerImp];
724  [vertical_scrollbar_painter setDelegate:nil];
725
726  [scrollbar_painter_controller_delegate_ invalidate];
727  [scrollbar_painter_controller_ setDelegate:nil];
728  [horizontal_scrollbar_painter_delegate_ invalidate];
729  [vertical_scrollbar_painter_delegate_ invalidate];
730  [scroll_animation_helper_delegate_ invalidate];
731  END_BLOCK_OBJC_EXCEPTIONS;
732
733  initial_scrollbar_paint_task_handle_.Cancel();
734  send_content_area_scrolled_task_handle_.Cancel();
735}
736
737ScrollResult ScrollAnimatorMac::UserScroll(
738    ScrollGranularity granularity,
739    const ScrollOffset& delta,
740    ScrollableArea::ScrollCallback on_finish) {
741  have_scrolled_since_page_load_ = true;
742
743  if (!scrollable_area_->ScrollAnimatorEnabled() ||
744      granularity == ScrollGranularity::kScrollByPixel ||
745      granularity == ScrollGranularity::kScrollByPrecisePixel) {
746    return ScrollAnimatorBase::UserScroll(granularity, delta,
747                                          std::move(on_finish));
748  }
749
750  // TODO(lanwei): we should find when the animation finishes and run the
751  // callback after the animation finishes, see https://crbug.com/967842.
752  if (on_finish)
753    std::move(on_finish).Run();
754
755  ScrollOffset consumed_delta = ComputeDeltaToConsume(delta);
756  ScrollOffset new_offset = current_offset_ + consumed_delta;
757  if (current_offset_ == new_offset)
758    return ScrollResult();
759
760  // Prevent clobbering an existing animation on an unscrolled axis.
761  if ([scroll_animation_helper_ _isAnimating]) {
762    NSPoint target_origin = [scroll_animation_helper_ targetOrigin];
763    if (!delta.Width())
764      new_offset.SetWidth(target_origin.x);
765    if (!delta.Height())
766      new_offset.SetHeight(target_origin.y);
767  }
768
769  NSPoint new_point = NSMakePoint(new_offset.Width(), new_offset.Height());
770  [scroll_animation_helper_ scrollToPoint:new_point];
771
772  // TODO(bokan): This has different semantics on ScrollResult than
773  // ScrollAnimator,
774  // which only returns unused delta if there's no animation and we don't start
775  // one.
776  return ScrollResult(consumed_delta.Width(), consumed_delta.Height(),
777                      delta.Width() - consumed_delta.Width(),
778                      delta.Height() - consumed_delta.Height());
779}
780
781void ScrollAnimatorMac::ScrollToOffsetWithoutAnimation(
782    const ScrollOffset& offset) {
783  [scroll_animation_helper_ _stopRun];
784  ImmediateScrollTo(offset);
785}
786
787ScrollOffset ScrollAnimatorMac::AdjustScrollOffsetIfNecessary(
788    const ScrollOffset& offset) const {
789  ScrollOffset min_offset = scrollable_area_->MinimumScrollOffset();
790  ScrollOffset max_offset = scrollable_area_->MaximumScrollOffset();
791
792  float new_x = clampTo<float, float>(offset.Width(), min_offset.Width(),
793                                      max_offset.Width());
794  float new_y = clampTo<float, float>(offset.Height(), min_offset.Height(),
795                                      max_offset.Height());
796
797  return ScrollOffset(new_x, new_y);
798}
799
800void ScrollAnimatorMac::ImmediateScrollTo(const ScrollOffset& new_offset) {
801  ScrollOffset adjusted_offset = AdjustScrollOffsetIfNecessary(new_offset);
802
803  bool offset_changed = adjusted_offset != current_offset_;
804  if (!offset_changed)
805    return;
806
807  ScrollOffset delta = adjusted_offset - current_offset_;
808
809  current_offset_ = adjusted_offset;
810  NotifyContentAreaScrolled(delta, mojom::blink::ScrollType::kUser);
811  NotifyOffsetChanged();
812}
813
814void ScrollAnimatorMac::ImmediateScrollToOffsetForScrollAnimation(
815    const ScrollOffset& new_offset) {
816  DCHECK(scroll_animation_helper_);
817  ImmediateScrollTo(new_offset);
818}
819
820void ScrollAnimatorMac::ContentAreaWillPaint() const {
821  if (!GetScrollableArea()->ScrollbarsCanBeActive())
822    return;
823  [scrollbar_painter_controller_ contentAreaWillDraw];
824}
825
826void ScrollAnimatorMac::MouseEnteredContentArea() const {
827  if (!GetScrollableArea()->ScrollbarsCanBeActive())
828    return;
829  [scrollbar_painter_controller_ mouseEnteredContentArea];
830}
831
832void ScrollAnimatorMac::MouseExitedContentArea() const {
833  if (!GetScrollableArea()->ScrollbarsCanBeActive())
834    return;
835  [scrollbar_painter_controller_ mouseExitedContentArea];
836}
837
838void ScrollAnimatorMac::MouseMovedInContentArea() const {
839  if (!GetScrollableArea()->ScrollbarsCanBeActive())
840    return;
841  [scrollbar_painter_controller_ mouseMovedInContentArea];
842}
843
844void ScrollAnimatorMac::MouseEnteredScrollbar(Scrollbar& scrollbar) const {
845  if (!GetScrollableArea()->ScrollbarsCanBeActive())
846    return;
847
848  if (!SupportsUIStateTransitionProgress())
849    return;
850  if (ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar))
851    [painter mouseEnteredScroller];
852}
853
854void ScrollAnimatorMac::MouseExitedScrollbar(Scrollbar& scrollbar) const {
855  if (!GetScrollableArea()->ScrollbarsCanBeActive())
856    return;
857
858  if (!SupportsUIStateTransitionProgress())
859    return;
860  if (ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar))
861    [painter mouseExitedScroller];
862}
863
864void ScrollAnimatorMac::ContentsResized() const {
865  if (!GetScrollableArea()->ScrollbarsCanBeActive())
866    return;
867  [scrollbar_painter_controller_ contentAreaDidResize];
868}
869
870void ScrollAnimatorMac::ContentAreaDidShow() const {
871  if (!GetScrollableArea()->ScrollbarsCanBeActive())
872    return;
873  [scrollbar_painter_controller_ windowOrderedIn];
874}
875
876void ScrollAnimatorMac::ContentAreaDidHide() const {
877  if (!GetScrollableArea()->ScrollbarsCanBeActive())
878    return;
879  [scrollbar_painter_controller_ windowOrderedOut];
880}
881
882void ScrollAnimatorMac::FinishCurrentScrollAnimations() {
883  [scrollbar_painter_controller_ hideOverlayScrollers];
884}
885
886void ScrollAnimatorMac::DidAddVerticalScrollbar(Scrollbar& scrollbar) {
887  ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar);
888  if (!painter)
889    return;
890
891  DCHECK(!vertical_scrollbar_painter_delegate_);
892  vertical_scrollbar_painter_delegate_.reset(
893      [[BlinkScrollbarPainterDelegate alloc] initWithScrollbar:&scrollbar]);
894
895  [painter setDelegate:vertical_scrollbar_painter_delegate_];
896  [scrollbar_painter_controller_ setVerticalScrollerImp:painter];
897}
898
899void ScrollAnimatorMac::WillRemoveVerticalScrollbar(Scrollbar& scrollbar) {
900  ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar);
901  DCHECK_EQ([scrollbar_painter_controller_ verticalScrollerImp], painter);
902
903  if (!painter)
904    DCHECK(!vertical_scrollbar_painter_delegate_);
905
906  [painter setDelegate:nil];
907  [vertical_scrollbar_painter_delegate_ invalidate];
908  vertical_scrollbar_painter_delegate_.reset();
909  [scrollbar_painter_controller_ setVerticalScrollerImp:nil];
910}
911
912void ScrollAnimatorMac::DidAddHorizontalScrollbar(Scrollbar& scrollbar) {
913  ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar);
914  if (!painter)
915    return;
916
917  DCHECK(!horizontal_scrollbar_painter_delegate_);
918  horizontal_scrollbar_painter_delegate_.reset(
919      [[BlinkScrollbarPainterDelegate alloc] initWithScrollbar:&scrollbar]);
920
921  [painter setDelegate:horizontal_scrollbar_painter_delegate_];
922  [scrollbar_painter_controller_ setHorizontalScrollerImp:painter];
923}
924
925void ScrollAnimatorMac::WillRemoveHorizontalScrollbar(Scrollbar& scrollbar) {
926  ScrollbarPainter painter = ScrollbarPainterForScrollbar(scrollbar);
927  DCHECK_EQ([scrollbar_painter_controller_ horizontalScrollerImp], painter);
928
929  if (!painter)
930    DCHECK(!horizontal_scrollbar_painter_delegate_);
931
932  [painter setDelegate:nil];
933  [horizontal_scrollbar_painter_delegate_ invalidate];
934  horizontal_scrollbar_painter_delegate_.reset();
935  [scrollbar_painter_controller_ setHorizontalScrollerImp:nil];
936}
937
938void ScrollAnimatorMac::NotifyContentAreaScrolled(
939    const ScrollOffset& delta,
940    mojom::blink::ScrollType scrollType) {
941  // This function is called when a page is going into the page cache, but the
942  // page
943  // isn't really scrolling in that case. We should only pass the message on to
944  // the
945  // ScrollbarPainterController when we're really scrolling on an active page.
946  if (IsExplicitScrollType(scrollType) &&
947      GetScrollableArea()->ScrollbarsCanBeActive())
948    SendContentAreaScrolledSoon(delta);
949}
950
951bool ScrollAnimatorMac::SetScrollbarsVisibleForTesting(bool show) {
952  if (show)
953    [scrollbar_painter_controller_ flashScrollers];
954  else
955    [scrollbar_painter_controller_ hideOverlayScrollers];
956
957  [vertical_scrollbar_painter_delegate_ updateVisibilityImmediately:show];
958  [horizontal_scrollbar_painter_delegate_ updateVisibilityImmediately:show];
959  return true;
960}
961
962void ScrollAnimatorMac::CancelAnimation() {
963  [scroll_animation_helper_ _stopRun];
964  have_scrolled_since_page_load_ = false;
965}
966
967void ScrollAnimatorMac::UpdateScrollerStyle() {
968  if (!GetScrollableArea()->ScrollbarsCanBeActive()) {
969    needs_scroller_style_update_ = true;
970    return;
971  }
972
973  blink::ScrollbarThemeMac* mac_theme =
974      MacOverlayScrollbarTheme(scrollable_area_->GetPageScrollbarTheme());
975  if (!mac_theme) {
976    needs_scroller_style_update_ = false;
977    return;
978  }
979
980  NSScrollerStyle new_style = [scrollbar_painter_controller_ scrollerStyle];
981
982  if (Scrollbar* vertical_scrollbar =
983          GetScrollableArea()->VerticalScrollbar()) {
984    vertical_scrollbar->SetNeedsPaintInvalidation(kAllParts);
985
986    ScrollbarPainter old_vertical_painter =
987        [scrollbar_painter_controller_ verticalScrollerImp];
988    ScrollbarPainter new_vertical_painter = [NSClassFromString(@"NSScrollerImp")
989        scrollerImpWithStyle:new_style
990                 controlSize:NSRegularControlSize
991                  horizontal:NO
992        replacingScrollerImp:old_vertical_painter];
993    [old_vertical_painter setDelegate:nil];
994    [new_vertical_painter setDelegate:vertical_scrollbar_painter_delegate_];
995    [scrollbar_painter_controller_ setVerticalScrollerImp:new_vertical_painter];
996    mac_theme->SetNewPainterForScrollbar(*vertical_scrollbar,
997                                         new_vertical_painter);
998
999    // The different scrollbar styles have different thicknesses, so we must
1000    // re-set the
1001    // frameRect to the new thickness, and the re-layout below will ensure the
1002    // offset
1003    // and length are properly updated.
1004    int thickness =
1005        mac_theme->ScrollbarThickness(vertical_scrollbar->ScaleFromDIP());
1006    vertical_scrollbar->SetFrameRect(IntRect(0, 0, thickness, thickness));
1007  }
1008
1009  if (Scrollbar* horizontal_scrollbar =
1010          GetScrollableArea()->HorizontalScrollbar()) {
1011    horizontal_scrollbar->SetNeedsPaintInvalidation(kAllParts);
1012
1013    ScrollbarPainter old_horizontal_painter =
1014        [scrollbar_painter_controller_ horizontalScrollerImp];
1015    ScrollbarPainter new_horizontal_painter =
1016        [NSClassFromString(@"NSScrollerImp")
1017            scrollerImpWithStyle:new_style
1018                     controlSize:NSRegularControlSize
1019                      horizontal:YES
1020            replacingScrollerImp:old_horizontal_painter];
1021    [old_horizontal_painter setDelegate:nil];
1022    [new_horizontal_painter setDelegate:horizontal_scrollbar_painter_delegate_];
1023    [scrollbar_painter_controller_
1024        setHorizontalScrollerImp:new_horizontal_painter];
1025    mac_theme->SetNewPainterForScrollbar(*horizontal_scrollbar,
1026                                         new_horizontal_painter);
1027
1028    // The different scrollbar styles have different thicknesses, so we must
1029    // re-set the
1030    // frameRect to the new thickness, and the re-layout below will ensure the
1031    // offset
1032    // and length are properly updated.
1033    int thickness =
1034        mac_theme->ScrollbarThickness(horizontal_scrollbar->ScaleFromDIP());
1035    horizontal_scrollbar->SetFrameRect(IntRect(0, 0, thickness, thickness));
1036  }
1037
1038  // If m_needsScrollerStyleUpdate is true, then the page is restoring from the
1039  // page cache, and
1040  // a relayout will happen on its own. Otherwise, we must initiate a re-layout
1041  // ourselves.
1042  if (!needs_scroller_style_update_)
1043    GetScrollableArea()->ScrollbarStyleChanged();
1044
1045  needs_scroller_style_update_ = false;
1046}
1047
1048void ScrollAnimatorMac::StartScrollbarPaintTimer() {
1049  // Post a task with 1 ms delay to give a chance to run other immediate tasks
1050  // that may cancel this.
1051  initial_scrollbar_paint_task_handle_ = PostDelayedCancellableTask(
1052      *task_runner_, FROM_HERE,
1053      WTF::Bind(&ScrollAnimatorMac::InitialScrollbarPaintTask,
1054                WrapWeakPersistent(this)),
1055      base::TimeDelta::FromMilliseconds(1));
1056}
1057
1058bool ScrollAnimatorMac::ScrollbarPaintTimerIsActive() const {
1059  return initial_scrollbar_paint_task_handle_.IsActive();
1060}
1061
1062void ScrollAnimatorMac::StopScrollbarPaintTimer() {
1063  initial_scrollbar_paint_task_handle_.Cancel();
1064}
1065
1066void ScrollAnimatorMac::InitialScrollbarPaintTask() {
1067  // To force the scrollbars to flash, we have to call hide first. Otherwise,
1068  // the ScrollbarPainterController
1069  // might think that the scrollbars are already showing and bail early.
1070  [scrollbar_painter_controller_ hideOverlayScrollers];
1071  [scrollbar_painter_controller_ flashScrollers];
1072}
1073
1074void ScrollAnimatorMac::SendContentAreaScrolledSoon(const ScrollOffset& delta) {
1075  content_area_scrolled_timer_scroll_delta_ = delta;
1076
1077  if (send_content_area_scrolled_task_handle_.IsActive())
1078    return;
1079  send_content_area_scrolled_task_handle_ = PostCancellableTask(
1080      *task_runner_, FROM_HERE,
1081      WTF::Bind(&ScrollAnimatorMac::SendContentAreaScrolledTask,
1082                WrapWeakPersistent(this)));
1083}
1084
1085void ScrollAnimatorMac::SendContentAreaScrolledTask() {
1086  if (SupportsContentAreaScrolledInDirection()) {
1087    [scrollbar_painter_controller_
1088        contentAreaScrolledInDirection:
1089            NSMakePoint(content_area_scrolled_timer_scroll_delta_.Width(),
1090                        content_area_scrolled_timer_scroll_delta_.Height())];
1091    content_area_scrolled_timer_scroll_delta_ = ScrollOffset();
1092  } else
1093    [scrollbar_painter_controller_ contentAreaScrolled];
1094}
1095
1096}  // namespace blink
1097