/* * 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 #import #import #import namespace WebCore { static ScrollbarThemeMac* macScrollbarTheme() { ScrollbarTheme& scrollbarTheme = ScrollbarTheme::theme(); return !scrollbarTheme.isMockTheme() ? static_cast(&scrollbarTheme) : nullptr; } static NSScrollerImp *scrollerImpForScrollbar(Scrollbar& scrollbar) { return ScrollbarThemeMac::painterForScrollbar(scrollbar); } } // namespace WebCore @interface WebScrollerImpPairDelegate : NSObject { 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(_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 _scrollerImp; FeatureToAnimate _featureToAnimate; CGFloat _startValue; CGFloat _endValue; NSTimeInterval _duration; RetainPtr _timer; RetainPtr _startDate; RefPtr _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 { WebCore::Scrollbar* _scrollbar; RetainPtr _knobAlphaAnimation; RetainPtr _trackAlphaAnimation; RetainPtr _uiStateTransitionAnimation; RetainPtr _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)scrollbarsController { return &static_cast(_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::ScrollbarOrientation::Vertical) 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&)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::ScrollbarOrientation::Vertical) { 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::create(ScrollableArea& scrollableArea) { return makeUnique(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(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(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(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(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)