| /* |
| * 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 <pal/spi/cg/CoreGraphicsSPI.h> |
| #import <pal/spi/cocoa/QuartzCoreSPI.h> |
| |
| #if PLATFORM(MAC) |
| #import <pal/spi/cocoa/NSColorSPI.h> |
| #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; |
| self.name = @"WebTextIndicatorLayer"; |
| |
| _textIndicator = &textIndicator; |
| _margin = margin; |
| |
| RefPtr<WebCore::NativeImage> 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<CGColorRef> 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<CALayer> 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<CALayer> 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<CALayer> 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<CALayer> 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<CAShapeLayer> 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<CAKeyframeAnimation> createBounceAnimation(CFTimeInterval duration) |
| { |
| RetainPtr<CAKeyframeAnimation> 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<CABasicAnimation> createContentCrossfadeAnimation(CFTimeInterval duration, WebCore::TextIndicator& textIndicator) |
| { |
| RetainPtr<CABasicAnimation> 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<CABasicAnimation> createShadowFadeAnimation(CFTimeInterval duration) |
| { |
| RetainPtr<CABasicAnimation> fadeShadowInAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"]; |
| [fadeShadowInAnimation setFromValue:@0]; |
| [fadeShadowInAnimation setToValue:@1]; |
| [fadeShadowInAnimation setFillMode:kCAFillModeForwards]; |
| [fadeShadowInAnimation setRemovedOnCompletion:NO]; |
| [fadeShadowInAnimation setDuration:duration]; |
| |
| return fadeShadowInAnimation; |
| } |
| |
| static RetainPtr<CABasicAnimation> createFadeInAnimation(CFTimeInterval duration) |
| { |
| RetainPtr<CABasicAnimation> 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<CAAnimation> presentationAnimation; |
| if (wantsBounce) |
| presentationAnimation = createBounceAnimation(animationDuration); |
| else if (wantsFadeIn) |
| presentationAnimation = createFadeInAnimation(animationDuration); |
| |
| RetainPtr<CABasicAnimation> crossfadeAnimation; |
| RetainPtr<CABasicAnimation> 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<CABasicAnimation> 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; |
| } |
| } |
| |
| @end |