/* * 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. */ #import "config.h" #import "WebTextIndicatorLayer.h" #import "GeometryUtilities.h" #import "GraphicsContext.h" #import "PathUtilities.h" #import "TextIndicator.h" #import "TextIndicatorWindow.h" #import "WebActionDisablingCALayerDelegate.h" #import #import #if PLATFORM(MAC) #import #endif constexpr CFTimeInterval bounceWithCrossfadeAnimationDuration = 0.3; constexpr CFTimeInterval fadeInAnimationDuration = 0.15; constexpr CFTimeInterval fadeOutAnimationDuration = 0.3; constexpr CGFloat borderWidth = 0; constexpr CGFloat cornerRadius = 0; constexpr CGFloat dropShadowOffsetX = 0; constexpr CGFloat dropShadowOffsetY = 1; constexpr NSString * const textLayerKey = @"TextLayer"; constexpr NSString * const dropShadowLayerKey = @"DropShadowLayer"; constexpr NSString * const rimShadowLayerKey = @"RimShadowLayer"; @implementation WebTextIndicatorLayer @synthesize fadingOut = _fadingOut; static bool indicatorWantsContentCrossfade(const WebCore::TextIndicator& indicator) { if (!indicator.data().contentImageWithHighlight) return false; switch (indicator.presentationTransition()) { case WebCore::TextIndicatorPresentationTransition::BounceAndCrossfade: return true; case WebCore::TextIndicatorPresentationTransition::Bounce: case WebCore::TextIndicatorPresentationTransition::FadeIn: case WebCore::TextIndicatorPresentationTransition::None: return false; } ASSERT_NOT_REACHED(); return false; } static bool indicatorWantsFadeIn(const WebCore::TextIndicator& indicator) { switch (indicator.presentationTransition()) { case WebCore::TextIndicatorPresentationTransition::FadeIn: return true; case WebCore::TextIndicatorPresentationTransition::Bounce: case WebCore::TextIndicatorPresentationTransition::BounceAndCrossfade: case WebCore::TextIndicatorPresentationTransition::None: return false; } ASSERT_NOT_REACHED(); return false; } - (bool)indicatorWantsBounce:(const WebCore::TextIndicator&)indicator { switch (indicator.presentationTransition()) { case WebCore::TextIndicatorPresentationTransition::BounceAndCrossfade: case WebCore::TextIndicatorPresentationTransition::Bounce: return true; case WebCore::TextIndicatorPresentationTransition::FadeIn: case WebCore::TextIndicatorPresentationTransition::None: return false; } ASSERT_NOT_REACHED(); return false; } - (bool)indicatorWantsManualAnimation:(const WebCore::TextIndicator&)indicator { switch (indicator.presentationTransition()) { case WebCore::TextIndicatorPresentationTransition::FadeIn: return true; case WebCore::TextIndicatorPresentationTransition::Bounce: case WebCore::TextIndicatorPresentationTransition::BounceAndCrossfade: case WebCore::TextIndicatorPresentationTransition::None: return false; } ASSERT_NOT_REACHED(); return false; } - (instancetype)initWithFrame:(CGRect)frame textIndicator:(WebCore::TextIndicator&)textIndicator margin:(CGSize)margin offset:(CGPoint)offset { if (!(self = [super init])) return nil; self.anchorPoint = CGPointZero; self.frame = frame; _textIndicator = &textIndicator; _margin = margin; RefPtr contentsImage; WebCore::FloatSize contentsImageLogicalSize { 1, 1 }; if (auto* contentImage = _textIndicator->contentImage()) { contentsImageLogicalSize = contentImage->size(); contentsImageLogicalSize.scale(1 / _textIndicator->contentImageScaleFactor()); if (indicatorWantsContentCrossfade(*_textIndicator) && _textIndicator->contentImageWithHighlight()) contentsImage = _textIndicator->contentImageWithHighlight()->nativeImage(); else contentsImage = contentImage->nativeImage(); } auto bounceLayers = adoptNS([[NSMutableArray alloc] init]); RetainPtr highlightColor; auto rimShadowColor = adoptCF(CGColorCreateGenericGray(0, 0.35)); auto dropShadowColor = adoptCF(CGColorCreateGenericGray(0, 0.2)); auto borderColor = adoptCF(CGColorCreateSRGB(0.96, 0.9, 0, 1)); #if PLATFORM(MAC) highlightColor = [NSColor findHighlightColor].CGColor; #else highlightColor = adoptCF(CGColorCreateSRGB(.99, .89, 0.22, 1.0)); #endif auto textRectsInBoundingRectCoordinates = _textIndicator->textRectsInBoundingRectCoordinates(); auto paths = WebCore::PathUtilities::pathsWithShrinkWrappedRects(textRectsInBoundingRectCoordinates, cornerRadius); for (const auto& path : paths) { WebCore::FloatRect pathBoundingRect = path.boundingRect(); WebCore::Path translatedPath; WebCore::AffineTransform transform; transform.translate(-pathBoundingRect.location()); translatedPath.addPath(path, transform); WebCore::FloatRect offsetTextRect = pathBoundingRect; offsetTextRect.move(offset.x, offset.y); WebCore::FloatRect bounceLayerRect = offsetTextRect; bounceLayerRect.move(_margin.width, _margin.height); RetainPtr bounceLayer = adoptNS([[CALayer alloc] init]); [bounceLayer setDelegate:[WebActionDisablingCALayerDelegate shared]]; [bounceLayer setFrame:bounceLayerRect]; [bounceLayer setOpacity:0]; [bounceLayers addObject:bounceLayer.get()]; WebCore::FloatRect yellowHighlightRect(WebCore::FloatPoint(), bounceLayerRect.size()); #if PLATFORM(MAC) RetainPtr dropShadowLayer = adoptNS([[CALayer alloc] init]); [dropShadowLayer setDelegate:[WebActionDisablingCALayerDelegate shared]]; [dropShadowLayer setShadowColor:dropShadowColor.get()]; [dropShadowLayer setShadowRadius:WebCore::dropShadowBlurRadius]; [dropShadowLayer setShadowOffset:CGSizeMake(dropShadowOffsetX, dropShadowOffsetY)]; [dropShadowLayer setShadowPath:translatedPath.platformPath()]; [dropShadowLayer setShadowOpacity:1]; [dropShadowLayer setFrame:yellowHighlightRect]; [bounceLayer addSublayer:dropShadowLayer.get()]; [bounceLayer setValue:dropShadowLayer.get() forKey:dropShadowLayerKey]; RetainPtr rimShadowLayer = adoptNS([[CALayer alloc] init]); [rimShadowLayer setDelegate:[WebActionDisablingCALayerDelegate shared]]; [rimShadowLayer setFrame:yellowHighlightRect]; [rimShadowLayer setShadowColor:rimShadowColor.get()]; [rimShadowLayer setShadowRadius:WebCore::rimShadowBlurRadius]; [rimShadowLayer setShadowPath:translatedPath.platformPath()]; [rimShadowLayer setShadowOffset:CGSizeZero]; [rimShadowLayer setShadowOpacity:1]; [rimShadowLayer setFrame:yellowHighlightRect]; [bounceLayer addSublayer:rimShadowLayer.get()]; [bounceLayer setValue:rimShadowLayer.get() forKey:rimShadowLayerKey]; #endif // PLATFORM(MAC) RetainPtr textLayer = adoptNS([[CALayer alloc] init]); [textLayer setBackgroundColor:highlightColor.get()]; [textLayer setBorderColor:borderColor.get()]; [textLayer setBorderWidth:borderWidth]; [textLayer setDelegate:[WebActionDisablingCALayerDelegate shared]]; if (contentsImage) [textLayer setContents:(__bridge id)contentsImage->platformImage().get()]; RetainPtr maskLayer = adoptNS([[CAShapeLayer alloc] init]); [maskLayer setPath:translatedPath.platformPath()]; [textLayer setMask:maskLayer.get()]; WebCore::FloatRect imageRect = pathBoundingRect; [textLayer setContentsRect:CGRectMake(imageRect.x() / contentsImageLogicalSize.width(), imageRect.y() / contentsImageLogicalSize.height(), imageRect.width() / contentsImageLogicalSize.width(), imageRect.height() / contentsImageLogicalSize.height())]; [textLayer setContentsGravity:kCAGravityCenter]; [textLayer setContentsScale:_textIndicator->contentImageScaleFactor()]; [textLayer setFrame:yellowHighlightRect]; [bounceLayer setValue:textLayer.get() forKey:textLayerKey]; [bounceLayer addSublayer:textLayer.get()]; } self.sublayers = bounceLayers.get(); _bounceLayers = bounceLayers; return self; } static RetainPtr createBounceAnimation(CFTimeInterval duration) { RetainPtr bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"]; [bounceAnimation setValues:@[ [NSValue valueWithCATransform3D:CATransform3DIdentity], [NSValue valueWithCATransform3D:CATransform3DMakeScale(WebCore::midBounceScale, WebCore::midBounceScale, 1)], [NSValue valueWithCATransform3D:CATransform3DIdentity] ]]; [bounceAnimation setDuration:duration]; return bounceAnimation; } static RetainPtr createContentCrossfadeAnimation(CFTimeInterval duration, WebCore::TextIndicator& textIndicator) { RetainPtr crossfadeAnimation = [CABasicAnimation animationWithKeyPath:@"contents"]; auto contentsImage = textIndicator.contentImage()->nativeImage(); [crossfadeAnimation setToValue:(__bridge id)contentsImage->platformImage().get()]; [crossfadeAnimation setFillMode:kCAFillModeForwards]; [crossfadeAnimation setRemovedOnCompletion:NO]; [crossfadeAnimation setDuration:duration]; return crossfadeAnimation; } static RetainPtr createShadowFadeAnimation(CFTimeInterval duration) { RetainPtr fadeShadowInAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; [fadeShadowInAnimation setFromValue:@0]; [fadeShadowInAnimation setToValue:@1]; [fadeShadowInAnimation setFillMode:kCAFillModeForwards]; [fadeShadowInAnimation setRemovedOnCompletion:NO]; [fadeShadowInAnimation setDuration:duration]; return fadeShadowInAnimation; } static RetainPtr createFadeInAnimation(CFTimeInterval duration) { RetainPtr fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; [fadeInAnimation setFromValue:@0]; [fadeInAnimation setToValue:@1]; [fadeInAnimation setFillMode:kCAFillModeForwards]; [fadeInAnimation setRemovedOnCompletion:NO]; [fadeInAnimation setDuration:duration]; return fadeInAnimation; } - (CFTimeInterval)_animationDuration { if ([self indicatorWantsBounce:*_textIndicator]) { if (indicatorWantsContentCrossfade(*_textIndicator)) return bounceWithCrossfadeAnimationDuration; return WebCore::bounceAnimationDuration.value(); } return fadeInAnimationDuration; } - (BOOL)hasCompletedAnimation { return _hasCompletedAnimation; } - (void)present { bool wantsBounce = [self indicatorWantsBounce:*_textIndicator]; bool wantsCrossfade = indicatorWantsContentCrossfade(*_textIndicator); bool wantsFadeIn = indicatorWantsFadeIn(*_textIndicator); CFTimeInterval animationDuration = [self _animationDuration]; _hasCompletedAnimation = false; RetainPtr presentationAnimation; if (wantsBounce) presentationAnimation = createBounceAnimation(animationDuration); else if (wantsFadeIn) presentationAnimation = createFadeInAnimation(animationDuration); RetainPtr crossfadeAnimation; RetainPtr fadeShadowInAnimation; if (wantsCrossfade) { crossfadeAnimation = createContentCrossfadeAnimation(animationDuration, *_textIndicator); fadeShadowInAnimation = createShadowFadeAnimation(animationDuration); } [CATransaction begin]; for (CALayer *bounceLayer in _bounceLayers.get()) { if ([self indicatorWantsManualAnimation:*_textIndicator]) bounceLayer.speed = 0; if (!wantsFadeIn) bounceLayer.opacity = 1; if (presentationAnimation) [bounceLayer addAnimation:presentationAnimation.get() forKey:@"presentation"]; if (wantsCrossfade) { [[bounceLayer valueForKey:textLayerKey] addAnimation:crossfadeAnimation.get() forKey:@"contentTransition"]; [[bounceLayer valueForKey:dropShadowLayerKey] addAnimation:fadeShadowInAnimation.get() forKey:@"fadeShadowIn"]; [[bounceLayer valueForKey:rimShadowLayerKey] addAnimation:fadeShadowInAnimation.get() forKey:@"fadeShadowIn"]; } } [CATransaction commit]; } - (void)hideWithCompletionHandler:(void(^)(void))completionHandler { RetainPtr fadeAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; [fadeAnimation setFromValue:@1]; [fadeAnimation setToValue:@0]; [fadeAnimation setFillMode:kCAFillModeForwards]; [fadeAnimation setRemovedOnCompletion:NO]; [fadeAnimation setDuration:fadeOutAnimationDuration]; [CATransaction begin]; [CATransaction setCompletionBlock:completionHandler]; [self addAnimation:fadeAnimation.get() forKey:@"fadeOut"]; [CATransaction commit]; } - (void)setAnimationProgress:(float)progress { if (_hasCompletedAnimation) return; if (progress == 1) { _hasCompletedAnimation = true; for (CALayer *bounceLayer in _bounceLayers.get()) { // Continue the animation from wherever it had manually progressed to. CFTimeInterval beginTime = bounceLayer.timeOffset; bounceLayer.speed = 1; beginTime = [bounceLayer convertTime:CACurrentMediaTime() fromLayer:nil] - beginTime; bounceLayer.beginTime = beginTime; } } else { CFTimeInterval animationDuration = [self _animationDuration]; for (CALayer *bounceLayer in _bounceLayers.get()) bounceLayer.timeOffset = progress * animationDuration; } } - (BOOL)isFlipped { return YES; } @end