blob: 4450ac1d9c69ec1c8281ffc0f683abf722201c74 [file] [log] [blame]
/*
* Copyright (C) 2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "ScrollbarsControllerMac.h"
#if PLATFORM(MAC)
#import "GraphicsLayer.h"
#import "Logging.h"
#import "NSScrollerImpDetails.h"
#import "ScrollAnimator.h"
#import "ScrollableArea.h"
#import "Scrollbar.h"
#import "ScrollbarThemeMac.h"
#import "TimingFunction.h"
#import "WheelEventTestMonitor.h" // FIXME: This is layering violation.
#import <pal/spi/mac/NSScrollerImpSPI.h>
#import <wtf/BlockObjCExceptions.h>
#import <wtf/NakedPtr.h>
#import <wtf/text/TextStream.h>
namespace WebCore {
static ScrollbarThemeMac* macScrollbarTheme()
{
ScrollbarTheme& scrollbarTheme = ScrollbarTheme::theme();
return !scrollbarTheme.isMockTheme() ? static_cast<ScrollbarThemeMac*>(&scrollbarTheme) : nullptr;
}
static NSScrollerImp *scrollerImpForScrollbar(Scrollbar& scrollbar)
{
return ScrollbarThemeMac::painterForScrollbar(scrollbar);
}
} // namespace WebCore
@interface WebScrollerImpPairDelegate : NSObject <NSScrollerImpPairDelegate> {
WebCore::ScrollableArea* _scrollableArea;
}
- (id)initWithScrollableArea:(WebCore::ScrollableArea*)scrollableArea;
@end
@implementation WebScrollerImpPairDelegate
- (id)initWithScrollableArea:(WebCore::ScrollableArea*)scrollableArea
{
self = [super init];
if (!self)
return nil;
_scrollableArea = scrollableArea;
return self;
}
- (void)invalidate
{
_scrollableArea = nullptr;
}
- (NSRect)contentAreaRectForScrollerImpPair:(NSScrollerImpPair *)scrollerImpPair
{
UNUSED_PARAM(scrollerImpPair);
if (!_scrollableArea)
return NSZeroRect;
WebCore::IntSize contentsSize = _scrollableArea->contentsSize();
return NSMakeRect(0, 0, contentsSize.width(), contentsSize.height());
}
- (BOOL)inLiveResizeForScrollerImpPair:(NSScrollerImpPair *)scrollerImpPair
{
UNUSED_PARAM(scrollerImpPair);
if (!_scrollableArea)
return NO;
return _scrollableArea->inLiveResize();
}
- (NSPoint)mouseLocationInContentAreaForScrollerImpPair:(NSScrollerImpPair *)scrollerImpPair
{
UNUSED_PARAM(scrollerImpPair);
if (!_scrollableArea)
return NSZeroPoint;
// It's OK that this position isn't relative to this scroller (which might be an overflow scroller).
// AppKit just takes the result and passes it back to -scrollerImpPair:convertContentPoint:toScrollerImp:.
return _scrollableArea->lastKnownMousePositionInView();
}
- (NSPoint)scrollerImpPair:(NSScrollerImpPair *)scrollerImpPair convertContentPoint:(NSPoint)pointInContentArea toScrollerImp:(NSScrollerImp *)scrollerImp
{
UNUSED_PARAM(scrollerImpPair);
if (!_scrollableArea || !scrollerImp)
return NSZeroPoint;
WebCore::Scrollbar* scrollbar = 0;
if ([scrollerImp isHorizontal])
scrollbar = _scrollableArea->horizontalScrollbar();
else
scrollbar = _scrollableArea->verticalScrollbar();
// It is possible to have a null scrollbar here since it is possible for this delegate
// method to be called between the moment when a scrollbar has been set to 0 and the
// moment when its destructor has been called. We should probably de-couple some
// of the clean-up work in ScrollbarThemeMac::unregisterScrollbar() to avoid this
// issue.
if (!scrollbar)
return NSZeroPoint;
ASSERT(scrollerImp == scrollerImpForScrollbar(*scrollbar));
return scrollbar->convertFromContainingView(WebCore::roundedIntPoint(pointInContentArea));
}
- (void)scrollerImpPair:(NSScrollerImpPair *)scrollerImpPair setContentAreaNeedsDisplayInRect:(NSRect)rect
{
UNUSED_PARAM(scrollerImpPair);
UNUSED_PARAM(rect);
if (!_scrollableArea)
return;
if ([scrollerImpPair overlayScrollerStateIsLocked])
return;
_scrollableArea->scrollbarsController().contentAreaWillPaint();
}
- (void)scrollerImpPair:(NSScrollerImpPair *)scrollerImpPair updateScrollerStyleForNewRecommendedScrollerStyle:(NSScrollerStyle)newRecommendedScrollerStyle
{
if (!_scrollableArea)
return;
[scrollerImpPair setScrollerStyle:newRecommendedScrollerStyle];
static_cast<WebCore::ScrollbarsControllerMac&>(_scrollableArea->scrollbarsController()).updateScrollerStyle();
}
@end
enum FeatureToAnimate {
ThumbAlpha,
TrackAlpha,
UIStateTransition,
ExpansionTransition
};
#if !LOG_DISABLED
static TextStream& operator<<(TextStream& ts, FeatureToAnimate feature)
{
switch (feature) {
case ThumbAlpha: ts << "ThumbAlpha" ; break;
case TrackAlpha: ts << "TrackAlpha" ; break;
case UIStateTransition: ts << "UIStateTransition" ; break;
case ExpansionTransition: ts << "ExpansionTransition" ; break;
}
return ts;
}
using WebCore::LogOverlayScrollbars;
#endif
@interface WebScrollbarPartAnimation : NSObject {
WebCore::Scrollbar* _scrollbar;
RetainPtr<NSScrollerImp> _scrollerImp;
FeatureToAnimate _featureToAnimate;
CGFloat _startValue;
CGFloat _endValue;
NSTimeInterval _duration;
RetainPtr<NSTimer> _timer;
RetainPtr<NSDate> _startDate;
RefPtr<WebCore::CubicBezierTimingFunction> _timingFunction;
}
- (id)initWithScrollbar:(WebCore::Scrollbar*)scrollbar featureToAnimate:(FeatureToAnimate)featureToAnimate animateFrom:(CGFloat)startValue animateTo:(CGFloat)endValue duration:(NSTimeInterval)duration;
- (void)setCurrentProgress:(NSTimer *)timer;
- (void)setDuration:(NSTimeInterval)duration;
- (void)stopAnimation;
@end
@implementation WebScrollbarPartAnimation
- (id)initWithScrollbar:(WebCore::Scrollbar*)scrollbar featureToAnimate:(FeatureToAnimate)featureToAnimate animateFrom:(CGFloat)startValue animateTo:(CGFloat)endValue duration:(NSTimeInterval)duration
{
self = [super init];
if (!self)
return nil;
const NSTimeInterval timeInterval = 0.01;
_timer = adoptNS([[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:0] interval:timeInterval target:self selector:@selector(setCurrentProgress:) userInfo:nil repeats:YES]);
_duration = duration;
_timingFunction = WebCore::CubicBezierTimingFunction::create(WebCore::CubicBezierTimingFunction::EaseInOut);
LOG_WITH_STREAM(OverlayScrollbars, stream << "Creating WebScrollbarPartAnimation for " << featureToAnimate << " from " << startValue << " to " << endValue);
_scrollbar = scrollbar;
_featureToAnimate = featureToAnimate;
_startValue = startValue;
_endValue = endValue;
return self;
}
- (void)startAnimation
{
ASSERT(_scrollbar);
_scrollerImp = scrollerImpForScrollbar(*_scrollbar);
LOG_WITH_STREAM(OverlayScrollbars, stream << "-[WebScrollbarPartAnimation " << self << "startAnimation] for " << _featureToAnimate);
[[NSRunLoop mainRunLoop] addTimer:_timer.get() forMode:NSDefaultRunLoopMode];
_startDate = adoptNS([[NSDate alloc] initWithTimeIntervalSinceNow:0]);
}
- (void)setStartValue:(CGFloat)startValue
{
_startValue = startValue;
}
- (void)setEndValue:(CGFloat)endValue
{
_endValue = endValue;
}
- (void)setCurrentProgress:(NSTimer *)timer
{
CGFloat progress = 0;
NSDate *now = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval elapsed = [now timeIntervalSinceDate:_startDate.get()];
if (elapsed > _duration) {
progress = 1;
[timer invalidate];
} else {
NSTimeInterval t = 1;
if (_duration)
t = elapsed / _duration;
progress = _timingFunction->transformProgress(t, _duration);
}
ASSERT(_scrollbar);
LOG_WITH_STREAM(OverlayScrollbars, stream << "-[" << self << " setCurrentProgress: " << progress << "] for " << _featureToAnimate);
CGFloat currentValue;
if (_startValue > _endValue)
currentValue = 1 - progress;
else
currentValue = progress;
switch (_featureToAnimate) {
case ThumbAlpha:
[_scrollerImp setKnobAlpha:currentValue];
break;
case TrackAlpha:
[_scrollerImp setTrackAlpha:currentValue];
break;
case UIStateTransition:
[_scrollerImp setUiStateTransitionProgress:currentValue];
break;
case ExpansionTransition:
[_scrollerImp setExpansionTransitionProgress:currentValue];
break;
}
if (!_scrollbar->supportsUpdateOnSecondaryThread())
_scrollbar->invalidate();
}
- (void)invalidate
{
BEGIN_BLOCK_OBJC_EXCEPTIONS
[self stopAnimation];
END_BLOCK_OBJC_EXCEPTIONS
_scrollbar = nullptr;
}
- (void)setDuration:(NSTimeInterval)duration
{
_duration = duration;
}
- (void)stopAnimation
{
[_timer invalidate];
}
@end
@interface WebScrollerImpDelegate : NSObject<NSAnimationDelegate, NSScrollerImpDelegate> {
WebCore::Scrollbar* _scrollbar;
RetainPtr<WebScrollbarPartAnimation> _knobAlphaAnimation;
RetainPtr<WebScrollbarPartAnimation> _trackAlphaAnimation;
RetainPtr<WebScrollbarPartAnimation> _uiStateTransitionAnimation;
RetainPtr<WebScrollbarPartAnimation> _expansionTransitionAnimation;
}
- (id)initWithScrollbar:(WebCore::Scrollbar*)scrollbar;
- (void)cancelAnimations;
@end
@implementation WebScrollerImpDelegate
- (id)initWithScrollbar:(WebCore::Scrollbar*)scrollbar
{
self = [super init];
if (!self)
return nil;
_scrollbar = scrollbar;
return self;
}
- (void)cancelAnimations
{
BEGIN_BLOCK_OBJC_EXCEPTIONS
[_knobAlphaAnimation stopAnimation];
[_trackAlphaAnimation stopAnimation];
[_uiStateTransitionAnimation stopAnimation];
[_expansionTransitionAnimation stopAnimation];
END_BLOCK_OBJC_EXCEPTIONS
}
- (NakedPtr<WebCore::ScrollbarsControllerMac>)scrollbarsController
{
return &static_cast<WebCore::ScrollbarsControllerMac&>(_scrollbar->scrollableArea().scrollbarsController());
}
- (NSRect)convertRectToBacking:(NSRect)aRect
{
return aRect;
}
- (NSRect)convertRectFromBacking:(NSRect)aRect
{
return aRect;
}
- (CALayer *)layer
{
if (!_scrollbar)
return nil;
if (!WebCore::ScrollbarThemeMac::isCurrentlyDrawingIntoLayer())
return nil;
WebCore::GraphicsLayer* layer;
if (_scrollbar->orientation() == WebCore::VerticalScrollbar)
layer = _scrollbar->scrollableArea().layerForVerticalScrollbar();
else
layer = _scrollbar->scrollableArea().layerForHorizontalScrollbar();
static CALayer *dummyLayer = [[CALayer alloc] init];
return layer ? layer->platformLayer() : dummyLayer;
}
- (NSPoint)mouseLocationInScrollerForScrollerImp:(NSScrollerImp *)scrollerImp
{
if (!_scrollbar)
return NSZeroPoint;
ASSERT_UNUSED(scrollerImp, scrollerImp == scrollerImpForScrollbar(*_scrollbar));
auto positionInView = _scrollbar->scrollableArea().lastKnownMousePositionInView();
return _scrollbar->convertFromContainingView(positionInView);
}
- (NSRect)convertRectToLayer:(NSRect)rect
{
return rect;
}
- (BOOL)shouldUseLayerPerPartForScrollerImp:(NSScrollerImp *)scrollerImp
{
UNUSED_PARAM(scrollerImp);
if (!_scrollbar)
return false;
return _scrollbar->supportsUpdateOnSecondaryThread();
}
#if HAVE(OS_DARK_MODE_SUPPORT)
- (NSAppearance *)effectiveAppearanceForScrollerImp:(NSScrollerImp *)scrollerImp
{
UNUSED_PARAM(scrollerImp);
if (!_scrollbar) {
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
return [NSAppearance currentAppearance];
ALLOW_DEPRECATED_DECLARATIONS_END
}
// Keep this in sync with FrameView::paintScrollCorner.
// The base system does not support dark Aqua, so we might get a null result.
bool useDarkAppearance = _scrollbar->scrollableArea().useDarkAppearanceForScrollbars();
if (auto *appearance = [NSAppearance appearanceNamed:useDarkAppearance ? NSAppearanceNameDarkAqua : NSAppearanceNameAqua])
return appearance;
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
return [NSAppearance currentAppearance];
ALLOW_DEPRECATED_DECLARATIONS_END
}
#endif
- (void)setUpAlphaAnimation:(RetainPtr<WebScrollbarPartAnimation>&)scrollbarPartAnimation scrollerPainter:(NSScrollerImp *)scrollerPainter part:(WebCore::ScrollbarPart)part animateAlphaTo:(CGFloat)newAlpha duration:(NSTimeInterval)duration
{
// If the user has scrolled the page, then the scrollbars must be animated here.
// This overrides the early returns.
bool mustAnimate = ![self scrollbarsController]->scrollbarAnimationsUnsuspendedByUserInteraction();
LOG_WITH_STREAM(OverlayScrollbars, stream << "WebScrollerImpDelegate for [" << _scrollbar->scrollableArea() << "] setUpAlphaAnimation: scrollbarAnimationsUnsuspendedByUserInteraction " << [self scrollbarsController]->scrollbarAnimationsUnsuspendedByUserInteraction() << " shouldSuspendScrollAnimations " << _scrollbar->scrollableArea().shouldSuspendScrollAnimations());
if ([self scrollbarsController]->scrollbarPaintTimerIsActive() && !mustAnimate)
return;
if ([self scrollbarsController]->shouldSuspendScrollbarAnimations() && !mustAnimate) {
[self scrollbarsController]->startScrollbarPaintTimer();
return;
}
// At this point, we are definitely going to animate now, so stop the timer.
[self scrollbarsController]->stopScrollbarPaintTimer();
// If we are currently animating, stop
if (scrollbarPartAnimation) {
[scrollbarPartAnimation stopAnimation];
scrollbarPartAnimation = nil;
}
if (auto* macTheme = WebCore::macScrollbarTheme())
macTheme->setPaintCharacteristicsForScrollbar(*_scrollbar);
if (part == WebCore::ThumbPart && _scrollbar->orientation() == WebCore::VerticalScrollbar) {
if (newAlpha == 1) {
auto thumbRect = WebCore::IntRect([scrollerPainter rectForPart:NSScrollerKnob]);
[self scrollbarsController]->setVisibleScrollerThumbRect(thumbRect);
} else
[self scrollbarsController]->setVisibleScrollerThumbRect({ });
}
scrollbarPartAnimation = adoptNS([[WebScrollbarPartAnimation alloc] initWithScrollbar:_scrollbar
featureToAnimate:part == WebCore::ThumbPart ? ThumbAlpha : TrackAlpha
animateFrom:part == WebCore::ThumbPart ? [scrollerPainter knobAlpha] : [scrollerPainter trackAlpha]
animateTo:newAlpha
duration:duration]);
[scrollbarPartAnimation startAnimation];
}
- (void)scrollerImp:(NSScrollerImp *)scrollerImp animateKnobAlphaTo:(CGFloat)newKnobAlpha duration:(NSTimeInterval)duration
{
if (!_scrollbar)
return;
ASSERT(scrollerImp == scrollerImpForScrollbar(*_scrollbar));
NSScrollerImp *scrollerPainter = (NSScrollerImp *)scrollerImp;
if (![self scrollbarsController]->scrollbarsCanBeActive()) {
[scrollerImp setKnobAlpha:0];
_scrollbar->invalidate();
return;
}
// If we are fading the scrollbar away, that is a good indication that we are no longer going to
// be moving it around on the scrolling thread. Calling [scrollerPainter setUsePresentationValue:NO]
// will pass that information on to the NSScrollerImp API.
if (newKnobAlpha == 0 && _scrollbar->supportsUpdateOnSecondaryThread())
[scrollerPainter setUsePresentationValue:NO];
[self setUpAlphaAnimation:_knobAlphaAnimation scrollerPainter:scrollerPainter part:WebCore::ThumbPart animateAlphaTo:newKnobAlpha duration:duration];
}
- (void)scrollerImp:(NSScrollerImp *)scrollerImp animateTrackAlphaTo:(CGFloat)newTrackAlpha duration:(NSTimeInterval)duration
{
if (!_scrollbar)
return;
ASSERT(scrollerImp == scrollerImpForScrollbar(*_scrollbar));
NSScrollerImp *scrollerPainter = (NSScrollerImp *)scrollerImp;
[self setUpAlphaAnimation:_trackAlphaAnimation scrollerPainter:scrollerPainter part:WebCore::BackTrackPart animateAlphaTo:newTrackAlpha duration:duration];
}
- (void)scrollerImp:(NSScrollerImp *)scrollerImp animateUIStateTransitionWithDuration:(NSTimeInterval)duration
{
if (!_scrollbar)
return;
ASSERT(scrollerImp == scrollerImpForScrollbar(*_scrollbar));
// UIStateTransition always animates to 1. In case an animation is in progress this avoids a hard transition.
[scrollerImp setUiStateTransitionProgress:1 - [scrollerImp uiStateTransitionProgress]];
// If the UI state transition is happening, then we are no longer moving the scrollbar on the scrolling thread.
if (_scrollbar->supportsUpdateOnSecondaryThread())
[scrollerImp setUsePresentationValue:NO];
if (!_uiStateTransitionAnimation) {
_uiStateTransitionAnimation = adoptNS([[WebScrollbarPartAnimation alloc] initWithScrollbar:_scrollbar
featureToAnimate:UIStateTransition
animateFrom:[scrollerImp uiStateTransitionProgress]
animateTo:1.0
duration:duration]);
} else {
// If we don't need to initialize the animation, just reset the values in case they have changed.
[_uiStateTransitionAnimation setStartValue:[scrollerImp uiStateTransitionProgress]];
[_uiStateTransitionAnimation setEndValue:1.0];
[_uiStateTransitionAnimation setDuration:duration];
}
[_uiStateTransitionAnimation startAnimation];
}
- (void)scrollerImp:(NSScrollerImp *)scrollerImp animateExpansionTransitionWithDuration:(NSTimeInterval)duration
{
if (!_scrollbar)
return;
ASSERT(scrollerImp == scrollerImpForScrollbar(*_scrollbar));
// ExpansionTransition always animates to 1. In case an animation is in progress this avoids a hard transition.
[scrollerImp setExpansionTransitionProgress:1 - [scrollerImp expansionTransitionProgress]];
if (!_expansionTransitionAnimation) {
_expansionTransitionAnimation = adoptNS([[WebScrollbarPartAnimation alloc] initWithScrollbar:_scrollbar
featureToAnimate:ExpansionTransition
animateFrom:[scrollerImp expansionTransitionProgress]
animateTo:1.0
duration:duration]);
} else {
// If we don't need to initialize the animation, just reset the values in case they have changed.
[_expansionTransitionAnimation setStartValue:[scrollerImp uiStateTransitionProgress]];
[_expansionTransitionAnimation setEndValue:1.0];
[_expansionTransitionAnimation setDuration:duration];
}
[_expansionTransitionAnimation startAnimation];
}
- (void)scrollerImp:(NSScrollerImp *)scrollerImp overlayScrollerStateChangedTo:(NSOverlayScrollerState)newOverlayScrollerState
{
UNUSED_PARAM(scrollerImp);
UNUSED_PARAM(newOverlayScrollerState);
}
- (void)invalidate
{
_scrollbar = 0;
BEGIN_BLOCK_OBJC_EXCEPTIONS
[_knobAlphaAnimation invalidate];
[_trackAlphaAnimation invalidate];
[_uiStateTransitionAnimation invalidate];
[_expansionTransitionAnimation invalidate];
END_BLOCK_OBJC_EXCEPTIONS
}
@end
namespace WebCore {
std::unique_ptr<ScrollbarsController> ScrollbarsController::create(ScrollableArea& scrollableArea)
{
return makeUnique<ScrollbarsControllerMac>(scrollableArea);
}
ScrollbarsControllerMac::ScrollbarsControllerMac(ScrollableArea& scrollableArea)
: ScrollbarsController(scrollableArea)
, m_initialScrollbarPaintTimer(*this, &ScrollbarsControllerMac::initialScrollbarPaintTimerFired)
, m_sendContentAreaScrolledTimer(*this, &ScrollbarsControllerMac::sendContentAreaScrolledTimerFired)
{
m_scrollerImpPairDelegate = adoptNS([[WebScrollerImpPairDelegate alloc] initWithScrollableArea:&scrollableArea]);
m_scrollerImpPair = adoptNS([[NSScrollerImpPair alloc] init]);
[m_scrollerImpPair setDelegate:m_scrollerImpPairDelegate.get()];
[m_scrollerImpPair setScrollerStyle:ScrollerStyle::recommendedScrollerStyle()];
}
ScrollbarsControllerMac::~ScrollbarsControllerMac()
{
BEGIN_BLOCK_OBJC_EXCEPTIONS
[m_scrollerImpPairDelegate invalidate];
[m_scrollerImpPair setDelegate:nil];
[m_horizontalScrollerImpDelegate invalidate];
[m_verticalScrollerImpDelegate invalidate];
END_BLOCK_OBJC_EXCEPTIONS
}
void ScrollbarsControllerMac::cancelAnimations()
{
LOG_WITH_STREAM(OverlayScrollbars, stream << "ScrollbarsControllerMac for [" << scrollableArea() << "] cancelAnimations");
if (scrollbarPaintTimerIsActive())
stopScrollbarPaintTimer();
[m_horizontalScrollerImpDelegate cancelAnimations];
[m_verticalScrollerImpDelegate cancelAnimations];
ScrollbarsController::cancelAnimations();
}
void ScrollbarsControllerMac::setVisibleScrollerThumbRect(const IntRect& scrollerThumb)
{
auto rectInViewCoordinates = scrollerThumb;
if (auto* verticalScrollbar = scrollableArea().verticalScrollbar())
rectInViewCoordinates = verticalScrollbar->convertToContainingView(scrollerThumb);
if (rectInViewCoordinates == m_visibleScrollerThumbRect)
return;
scrollableArea().setVisibleScrollerThumbRect(rectInViewCoordinates);
m_visibleScrollerThumbRect = rectInViewCoordinates;
}
void ScrollbarsControllerMac::contentAreaWillPaint() const
{
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
[m_scrollerImpPair contentAreaWillDraw];
ALLOW_DEPRECATED_DECLARATIONS_END
}
void ScrollbarsControllerMac::mouseEnteredContentArea()
{
LOG_WITH_STREAM(OverlayScrollbars, stream << "ScrollbarsControllerMac for [" << scrollableArea() << "] mouseEnteredContentArea");
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair mouseEnteredContentArea];
}
void ScrollbarsControllerMac::mouseExitedContentArea()
{
LOG_WITH_STREAM(OverlayScrollbars, stream << "ScrollbarsControllerMac for [" << scrollableArea() << "] mouseExitedContentArea");
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair mouseExitedContentArea];
}
void ScrollbarsControllerMac::mouseMovedInContentArea()
{
LOG_WITH_STREAM(OverlayScrollbars, stream << "ScrollbarsControllerMac for [" << scrollableArea() << "] mouseMovedInContentArea");
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair mouseMovedInContentArea];
}
void ScrollbarsControllerMac::mouseEnteredScrollbar(Scrollbar* scrollbar) const
{
// At this time, only legacy scrollbars needs to send notifications here.
if (ScrollerStyle::recommendedScrollerStyle() != NSScrollerStyleLegacy)
return;
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
if (NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar))
[painter mouseEnteredScroller];
}
void ScrollbarsControllerMac::mouseExitedScrollbar(Scrollbar* scrollbar) const
{
// At this time, only legacy scrollbars needs to send notifications here.
if (ScrollerStyle::recommendedScrollerStyle() != NSScrollerStyleLegacy)
return;
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
if (NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar))
[painter mouseExitedScroller];
}
void ScrollbarsControllerMac::mouseIsDownInScrollbar(Scrollbar* scrollbar, bool mouseIsDown) const
{
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
if (NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar)) {
[painter setTracking:mouseIsDown];
if (mouseIsDown)
[m_scrollerImpPair beginScrollGesture];
else
[m_scrollerImpPair endScrollGesture];
}
}
void ScrollbarsControllerMac::willStartLiveResize()
{
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair startLiveResize];
}
void ScrollbarsControllerMac::contentsSizeChanged() const
{
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair contentAreaDidResize];
}
void ScrollbarsControllerMac::willEndLiveResize()
{
ScrollbarsController::willEndLiveResize();
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair endLiveResize];
}
void ScrollbarsControllerMac::contentAreaDidShow()
{
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair windowOrderedIn];
}
void ScrollbarsControllerMac::contentAreaDidHide()
{
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair windowOrderedOut];
}
void ScrollbarsControllerMac::didBeginScrollGesture()
{
LOG_WITH_STREAM(OverlayScrollbars, stream << "ScrollbarsControllerMac for [" << scrollableArea() << "] didBeginScrollGesture");
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair beginScrollGesture];
if (auto* monitor = wheelEventTestMonitor())
monitor->deferForReason(reinterpret_cast<WheelEventTestMonitor::ScrollableAreaIdentifier>(this), WheelEventTestMonitor::ContentScrollInProgress);
ScrollbarsController::didBeginScrollGesture();
}
void ScrollbarsControllerMac::didEndScrollGesture()
{
LOG_WITH_STREAM(OverlayScrollbars, stream << "ScrollbarsControllerMac for [" << scrollableArea() << "] didEndScrollGesture");
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair endScrollGesture];
if (auto* monitor = wheelEventTestMonitor())
monitor->removeDeferralForReason(reinterpret_cast<WheelEventTestMonitor::ScrollableAreaIdentifier>(this), WheelEventTestMonitor::ContentScrollInProgress);
ScrollbarsController::didEndScrollGesture();
}
void ScrollbarsControllerMac::mayBeginScrollGesture()
{
LOG_WITH_STREAM(OverlayScrollbars, stream << "ScrollbarsControllerMac for [" << scrollableArea() << "] mayBeginScrollGesture");
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
[m_scrollerImpPair beginScrollGesture];
[m_scrollerImpPair contentAreaScrolled];
ScrollbarsController::mayBeginScrollGesture();
}
void ScrollbarsControllerMac::lockOverlayScrollbarStateToHidden(bool shouldLockState)
{
if (shouldLockState)
[m_scrollerImpPair lockOverlayScrollerState:NSOverlayScrollerStateHidden];
else {
[m_scrollerImpPair unlockOverlayScrollerState];
// We never update scroller style for PainterControllers that are locked. If we have a pending
// need to update the style, do it once we've unlocked the scroller state.
if (m_needsScrollerStyleUpdate)
updateScrollerStyle();
}
}
bool ScrollbarsControllerMac::scrollbarsCanBeActive() const
{
return ![m_scrollerImpPair overlayScrollerStateIsLocked];
}
void ScrollbarsControllerMac::didAddVerticalScrollbar(Scrollbar* scrollbar)
{
NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar);
if (!painter)
return;
ASSERT(!m_verticalScrollerImpDelegate);
m_verticalScrollerImpDelegate = adoptNS([[WebScrollerImpDelegate alloc] initWithScrollbar:scrollbar]);
[painter setDelegate:m_verticalScrollerImpDelegate.get()];
if (GraphicsLayer* layer = scrollbar->scrollableArea().layerForVerticalScrollbar())
[painter setLayer:layer->platformLayer()];
[m_scrollerImpPair setVerticalScrollerImp:painter];
if (scrollableArea().inLiveResize())
[painter setKnobAlpha:1];
}
void ScrollbarsControllerMac::willRemoveVerticalScrollbar(Scrollbar* scrollbar)
{
NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar);
if (!painter)
return;
ASSERT(m_verticalScrollerImpDelegate);
[m_verticalScrollerImpDelegate invalidate];
m_verticalScrollerImpDelegate = nullptr;
[painter setDelegate:nil];
[m_scrollerImpPair setVerticalScrollerImp:nil];
}
void ScrollbarsControllerMac::didAddHorizontalScrollbar(Scrollbar* scrollbar)
{
NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar);
if (!painter)
return;
ASSERT(!m_horizontalScrollerImpDelegate);
m_horizontalScrollerImpDelegate = adoptNS([[WebScrollerImpDelegate alloc] initWithScrollbar:scrollbar]);
[painter setDelegate:m_horizontalScrollerImpDelegate.get()];
if (GraphicsLayer* layer = scrollbar->scrollableArea().layerForHorizontalScrollbar())
[painter setLayer:layer->platformLayer()];
[m_scrollerImpPair setHorizontalScrollerImp:painter];
if (scrollableArea().inLiveResize())
[painter setKnobAlpha:1];
}
void ScrollbarsControllerMac::willRemoveHorizontalScrollbar(Scrollbar* scrollbar)
{
NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar);
if (!painter)
return;
ASSERT(m_horizontalScrollerImpDelegate);
[m_horizontalScrollerImpDelegate invalidate];
m_horizontalScrollerImpDelegate = nullptr;
[painter setDelegate:nil];
[m_scrollerImpPair setHorizontalScrollerImp:nil];
}
void ScrollbarsControllerMac::invalidateScrollbarPartLayers(Scrollbar* scrollbar)
{
NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar);
[painter setNeedsDisplay:YES];
}
void ScrollbarsControllerMac::verticalScrollbarLayerDidChange()
{
GraphicsLayer* layer = scrollableArea().layerForVerticalScrollbar();
Scrollbar* scrollbar = scrollableArea().verticalScrollbar();
if (!scrollbar)
return;
NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar);
if (!painter)
return;
[painter setLayer:layer ? layer->platformLayer() : nil];
}
void ScrollbarsControllerMac::horizontalScrollbarLayerDidChange()
{
GraphicsLayer* layer = scrollableArea().layerForHorizontalScrollbar();
Scrollbar* scrollbar = scrollableArea().horizontalScrollbar();
if (!scrollbar)
return;
NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar);
if (!painter)
return;
[painter setLayer:layer ? layer->platformLayer() : nil];
}
bool ScrollbarsControllerMac::shouldScrollbarParticipateInHitTesting(Scrollbar* scrollbar)
{
// Non-overlay scrollbars should always participate in hit testing.
if (ScrollerStyle::recommendedScrollerStyle() != NSScrollerStyleOverlay)
return true;
// Overlay scrollbars should participate in hit testing whenever they are at all visible.
NSScrollerImp *painter = scrollerImpForScrollbar(*scrollbar);
if (!painter)
return false;
return [painter knobAlpha] > 0;
}
void ScrollbarsControllerMac::notifyContentAreaScrolled(const FloatSize& delta)
{
// This function is called when a page is going into the back/forward cache, but the page
// isn't really scrolling in that case. We should only pass the message on to the
// ScrollerImpPair when we're really scrolling on an active page.
if ([m_scrollerImpPair overlayScrollerStateIsLocked])
return;
if (scrollableArea().isHandlingWheelEvent())
sendContentAreaScrolled(delta);
else
sendContentAreaScrolledSoon(delta);
}
void ScrollbarsControllerMac::updateScrollerStyle()
{
if ([m_scrollerImpPair overlayScrollerStateIsLocked]) {
m_needsScrollerStyleUpdate = true;
return;
}
auto* macTheme = macScrollbarTheme();
if (!macTheme) {
m_needsScrollerStyleUpdate = false;
return;
}
macTheme->usesOverlayScrollbarsChanged();
NSScrollerStyle newStyle = [m_scrollerImpPair scrollerStyle];
if (Scrollbar* verticalScrollbar = scrollableArea().verticalScrollbar()) {
verticalScrollbar->invalidate();
NSScrollerImp *oldVerticalPainter = [m_scrollerImpPair verticalScrollerImp];
auto newVerticalPainter = retainPtr([NSScrollerImp scrollerImpWithStyle:newStyle controlSize:(NSControlSize)verticalScrollbar->controlSize() horizontal:NO replacingScrollerImp:oldVerticalPainter]);
[m_scrollerImpPair setVerticalScrollerImp:newVerticalPainter.get()];
macTheme->setNewPainterForScrollbar(*verticalScrollbar, WTFMove(newVerticalPainter));
macTheme->didCreateScrollerImp(*verticalScrollbar);
// The different scrollbar styles have different thicknesses, so we must re-set the
// frameRect to the new thickness, and the re-layout below will ensure the position
// and length are properly updated.
int thickness = macTheme->scrollbarThickness(verticalScrollbar->controlSize());
verticalScrollbar->setFrameRect(IntRect(0, 0, thickness, thickness));
}
if (Scrollbar* horizontalScrollbar = scrollableArea().horizontalScrollbar()) {
horizontalScrollbar->invalidate();
NSScrollerImp *oldHorizontalPainter = [m_scrollerImpPair horizontalScrollerImp];
auto newHorizontalPainter = retainPtr([NSScrollerImp scrollerImpWithStyle:newStyle controlSize:(NSControlSize)horizontalScrollbar->controlSize() horizontal:YES replacingScrollerImp:oldHorizontalPainter]);
[m_scrollerImpPair setHorizontalScrollerImp:newHorizontalPainter.get()];
macTheme->setNewPainterForScrollbar(*horizontalScrollbar, WTFMove(newHorizontalPainter));
macTheme->didCreateScrollerImp(*horizontalScrollbar);
// The different scrollbar styles have different thicknesses, so we must re-set the
// frameRect to the new thickness, and the re-layout below will ensure the position
// and length are properly updated.
int thickness = macTheme->scrollbarThickness(horizontalScrollbar->controlSize());
horizontalScrollbar->setFrameRect(IntRect(0, 0, thickness, thickness));
}
// If m_needsScrollerStyleUpdate is true, then the page is restoring from the back/forward cache, and
// a relayout will happen on its own. Otherwise, we must initiate a re-layout ourselves.
scrollableArea().scrollbarStyleChanged(newStyle == NSScrollerStyleOverlay ? ScrollbarStyle::Overlay : ScrollbarStyle::AlwaysVisible, !m_needsScrollerStyleUpdate);
m_needsScrollerStyleUpdate = false;
}
void ScrollbarsControllerMac::startScrollbarPaintTimer()
{
m_initialScrollbarPaintTimer.startOneShot(100_ms);
}
bool ScrollbarsControllerMac::scrollbarPaintTimerIsActive() const
{
return m_initialScrollbarPaintTimer.isActive();
}
void ScrollbarsControllerMac::stopScrollbarPaintTimer()
{
m_initialScrollbarPaintTimer.stop();
}
void ScrollbarsControllerMac::initialScrollbarPaintTimerFired()
{
LOG_WITH_STREAM(OverlayScrollbars, stream << "WebScrollerImpDelegate for [" << scrollableArea() << "] initialScrollbarPaintTimerFired - flashing scrollers");
// To force the scrollbars to flash, we have to call hide first. Otherwise, the ScrollerImpPair
// might think that the scrollbars are already showing and bail early.
[m_scrollerImpPair hideOverlayScrollers];
[m_scrollerImpPair flashScrollers];
}
void ScrollbarsControllerMac::sendContentAreaScrolledTimerFired()
{
sendContentAreaScrolled(m_contentAreaScrolledTimerScrollDelta);
m_contentAreaScrolledTimerScrollDelta = { };
if (auto* monitor = wheelEventTestMonitor())
monitor->removeDeferralForReason(reinterpret_cast<WheelEventTestMonitor::ScrollableAreaIdentifier>(this), WheelEventTestMonitor::ContentScrollInProgress);
}
void ScrollbarsControllerMac::sendContentAreaScrolledSoon(const FloatSize& delta)
{
m_contentAreaScrolledTimerScrollDelta = delta;
if (!m_sendContentAreaScrolledTimer.isActive())
m_sendContentAreaScrolledTimer.startOneShot(0_s);
if (auto* monitor = wheelEventTestMonitor())
monitor->deferForReason(reinterpret_cast<WheelEventTestMonitor::ScrollableAreaIdentifier>(this), WheelEventTestMonitor::ContentScrollInProgress);
}
void ScrollbarsControllerMac::sendContentAreaScrolled(const FloatSize& delta)
{
[m_scrollerImpPair contentAreaScrolledInDirection:NSMakePoint(delta.width(), delta.height())];
}
static String scrollbarState(Scrollbar* scrollbar)
{
if (!scrollbar)
return "none"_s;
StringBuilder result;
result.append(scrollbar->enabled() ? "enabled"_s : "disabled"_s);
if (!scrollbar->isOverlayScrollbar())
return result.toString();
NSScrollerImp *scrollerImp = scrollerImpForScrollbar(*scrollbar);
if (!scrollerImp)
return result.toString();
if (scrollerImp.expanded)
result.append(",expanded"_s);
if (scrollerImp.trackAlpha > 0)
result.append(",visible_track"_s);
if (scrollerImp.knobAlpha > 0)
result.append(",visible_thumb"_s);
return result.toString();
}
String ScrollbarsControllerMac::horizontalScrollbarStateForTesting() const
{
return scrollbarState(scrollableArea().horizontalScrollbar());
}
String ScrollbarsControllerMac::verticalScrollbarStateForTesting() const
{
return scrollbarState(scrollableArea().verticalScrollbar());
}
WheelEventTestMonitor* ScrollbarsControllerMac::wheelEventTestMonitor() const
{
return scrollableArea().scrollAnimator().wheelEventTestMonitor();
}
} // namespace WebCore
#endif // PLATFORM(MAC)