Make explicit which TextIndicator animations are driven manually, and which run autom...
[WebKit-https.git] / Source / WebCore / page / mac / TextIndicatorWindow.mm
1 /*
2  * Copyright (C) 2010 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "TextIndicatorWindow.h"
28
29 #if PLATFORM(MAC)
30
31 #import "GraphicsContext.h"
32 #import "QuartzCoreSPI.h"
33 #import "TextIndicator.h"
34 #import "WebActionDisablingCALayerDelegate.h"
35
36 const CFTimeInterval bounceAnimationDuration = 0.12;
37 const CFTimeInterval bounceWithCrossfadeAnimationDuration = 0.3;
38 const CFTimeInterval fadeInAnimationDuration = 0.15;
39 const CFTimeInterval timeBeforeFadeStarts = bounceAnimationDuration + 0.2;
40 const CFTimeInterval fadeOutAnimationDuration = 0.3;
41
42 #if ENABLE(LEGACY_TEXT_INDICATOR_STYLE)
43 const CGFloat midBounceScale = 1.5;
44 const CGFloat horizontalBorder = 3;
45 const CGFloat verticalBorder = 1;
46 const CGFloat borderWidth = 1.0;
47 const CGFloat cornerRadius = 3;
48 const CGFloat dropShadowOffsetX = 0;
49 const CGFloat dropShadowOffsetY = 1;
50 const CGFloat dropShadowBlurRadius = 1.5;
51 #else
52 const CGFloat midBounceScale = 1.25;
53 const CGFloat horizontalBorder = 2;
54 const CGFloat verticalBorder = 1;
55 const CGFloat borderWidth = 0;
56 const CGFloat cornerRadius = 0;
57 const CGFloat dropShadowOffsetX = 0;
58 const CGFloat dropShadowOffsetY = 1;
59 const CGFloat dropShadowBlurRadius = 2;
60 const CGFloat rimShadowBlurRadius = 1;
61 #endif
62
63 NSString *textLayerKey = @"TextLayer";
64 NSString *dropShadowLayerKey = @"DropShadowLayer";
65 NSString *rimShadowLayerKey = @"RimShadowLayer";
66
67 using namespace WebCore;
68
69 @interface WebTextIndicatorView : NSView {
70     RefPtr<TextIndicator> _textIndicator;
71     RetainPtr<NSArray> _bounceLayers;
72     NSSize _margin;
73     bool _hasCompletedAnimation;
74 }
75
76 - (instancetype)initWithFrame:(NSRect)frame textIndicator:(PassRefPtr<TextIndicator>)textIndicator margin:(NSSize)margin;
77
78 - (void)present;
79 - (void)hideWithCompletionHandler:(void(^)(void))completionHandler;
80
81 - (void)setAnimationProgress:(float)progress;
82
83 @end
84
85 @implementation WebTextIndicatorView
86
87 - (instancetype)initWithFrame:(NSRect)frame textIndicator:(PassRefPtr<TextIndicator>)textIndicator margin:(NSSize)margin
88 {
89     if (!(self = [super initWithFrame:frame]))
90         return nil;
91
92     _textIndicator = textIndicator;
93     _margin = margin;
94
95     self.wantsLayer = YES;
96     self.layer.anchorPoint = CGPointZero;
97
98     bool wantsCrossfade = _textIndicator->wantsContentCrossfade();
99
100     FloatSize contentsImageLogicalSize = _textIndicator->contentImage()->size();
101     contentsImageLogicalSize.scale(1 / _textIndicator->contentImageScaleFactor());
102     RetainPtr<CGImageRef> contentsImage;
103     if (wantsCrossfade)
104         contentsImage = _textIndicator->contentImageWithHighlight()->getCGImageRef();
105     else
106         contentsImage = _textIndicator->contentImage()->getCGImageRef();
107
108     RetainPtr<NSMutableArray> bounceLayers = adoptNS([[NSMutableArray alloc] init]);
109
110     RetainPtr<CGColorRef> highlightColor = [NSColor colorWithDeviceRed:1 green:1 blue:0 alpha:1].CGColor;
111     RetainPtr<CGColorRef> rimShadowColor = [NSColor colorWithDeviceWhite:0 alpha:0.35].CGColor;
112     RetainPtr<CGColorRef> dropShadowColor = [NSColor colorWithDeviceWhite:0 alpha:0.2].CGColor;
113
114     RetainPtr<CGColorRef> borderColor = [NSColor colorWithDeviceRed:.96 green:.90 blue:0 alpha:1].CGColor;
115     RetainPtr<CGColorRef> gradientDarkColor = [NSColor colorWithDeviceRed:.929 green:.8 blue:0 alpha:1].CGColor;
116     RetainPtr<CGColorRef> gradientLightColor = [NSColor colorWithDeviceRed:.949 green:.937 blue:0 alpha:1].CGColor;
117
118     for (auto& textRect : _textIndicator->textRectsInBoundingRectCoordinates()) {
119         FloatRect bounceLayerRect = textRect;
120         bounceLayerRect.move(_margin.width, _margin.height);
121         bounceLayerRect.inflateX(horizontalBorder);
122         bounceLayerRect.inflateY(verticalBorder);
123
124         RetainPtr<CALayer> bounceLayer = adoptNS([[CALayer alloc] init]);
125         [bounceLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
126         [bounceLayer setFrame:bounceLayerRect];
127         [bounceLayer setOpacity:0];
128         [bounceLayers addObject:bounceLayer.get()];
129
130         FloatRect yellowHighlightRect(FloatPoint(), bounceLayerRect.size());
131         // FIXME (138888): Ideally we wouldn't remove the margin in this case, but we need to
132         // ensure that the yellow highlight and contentImageWithHighlight overlap precisely.
133         if (wantsCrossfade) {
134             yellowHighlightRect.inflateX(-horizontalBorder);
135             yellowHighlightRect.inflateY(-verticalBorder);
136         }
137
138         RetainPtr<CALayer> dropShadowLayer = adoptNS([[CALayer alloc] init]);
139         [dropShadowLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
140         [dropShadowLayer setShadowColor:dropShadowColor.get()];
141         [dropShadowLayer setShadowRadius:dropShadowBlurRadius];
142         [dropShadowLayer setShadowOffset:CGSizeMake(dropShadowOffsetX, dropShadowOffsetY)];
143         [dropShadowLayer setShadowPathIsBounds:YES];
144         [dropShadowLayer setShadowOpacity:1];
145         [dropShadowLayer setFrame:yellowHighlightRect];
146         [dropShadowLayer setCornerRadius:cornerRadius];
147         [bounceLayer addSublayer:dropShadowLayer.get()];
148         [bounceLayer setValue:dropShadowLayer.get() forKey:dropShadowLayerKey];
149
150 #if !ENABLE(LEGACY_TEXT_INDICATOR_STYLE)
151         RetainPtr<CALayer> rimShadowLayer = adoptNS([[CALayer alloc] init]);
152         [rimShadowLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
153         [rimShadowLayer setFrame:yellowHighlightRect];
154         [rimShadowLayer setShadowColor:rimShadowColor.get()];
155         [rimShadowLayer setShadowRadius:rimShadowBlurRadius];
156         [rimShadowLayer setShadowPathIsBounds:YES];
157         [rimShadowLayer setShadowOffset:CGSizeZero];
158         [rimShadowLayer setShadowOpacity:1];
159         [rimShadowLayer setFrame:yellowHighlightRect];
160         [rimShadowLayer setCornerRadius:cornerRadius];
161         [bounceLayer addSublayer:rimShadowLayer.get()];
162         [bounceLayer setValue:rimShadowLayer.get() forKey:rimShadowLayerKey];
163 #endif
164
165 #if ENABLE(LEGACY_TEXT_INDICATOR_STYLE)
166         RetainPtr<CAGradientLayer> textLayer = adoptNS([[CAGradientLayer alloc] init]);
167         [textLayer setColors:@[ (id)gradientLightColor.get(), (id)gradientDarkColor.get() ]];
168 #else
169         RetainPtr<CALayer> textLayer = adoptNS([[CALayer alloc] init]);
170 #endif
171         [textLayer setBackgroundColor:highlightColor.get()];
172         [textLayer setBorderColor:borderColor.get()];
173         [textLayer setBorderWidth:borderWidth];
174         [textLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
175         [textLayer setContents:(id)contentsImage.get()];
176
177         FloatRect imageRect = textRect;
178         imageRect.move(_textIndicator->textBoundingRectInWindowCoordinates().location() - _textIndicator->selectionRectInWindowCoordinates().location());
179         [textLayer setContentsRect:CGRectMake(imageRect.x() / contentsImageLogicalSize.width(), imageRect.y() / contentsImageLogicalSize.height(), imageRect.width() / contentsImageLogicalSize.width(), imageRect.height() / contentsImageLogicalSize.height())];
180         [textLayer setContentsGravity:kCAGravityCenter];
181         [textLayer setContentsScale:_textIndicator->contentImageScaleFactor()];
182         [textLayer setFrame:yellowHighlightRect];
183         [textLayer setCornerRadius:cornerRadius];
184         [bounceLayer setValue:textLayer.get() forKey:textLayerKey];
185         [bounceLayer addSublayer:textLayer.get()];
186     }
187
188     self.layer.sublayers = bounceLayers.get();
189     _bounceLayers = bounceLayers;
190
191     return self;
192 }
193
194 static RetainPtr<CAKeyframeAnimation> createBounceAnimation(CFTimeInterval duration)
195 {
196     RetainPtr<CAKeyframeAnimation> bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
197     [bounceAnimation setValues:@[
198         [NSValue valueWithCATransform3D:CATransform3DIdentity],
199         [NSValue valueWithCATransform3D:CATransform3DMakeScale(midBounceScale, midBounceScale, 1)],
200         [NSValue valueWithCATransform3D:CATransform3DIdentity]
201         ]];
202     [bounceAnimation setDuration:duration];
203
204     return bounceAnimation;
205 }
206
207 static RetainPtr<CABasicAnimation> createContentCrossfadeAnimation(CFTimeInterval duration, TextIndicator& textIndicator)
208 {
209     RetainPtr<CABasicAnimation> crossfadeAnimation = [CABasicAnimation animationWithKeyPath:@"contents"];
210     RetainPtr<CGImageRef> contentsImage = textIndicator.contentImage()->getCGImageRef();
211     [crossfadeAnimation setToValue:(id)contentsImage.get()];
212     [crossfadeAnimation setFillMode:kCAFillModeForwards];
213     [crossfadeAnimation setRemovedOnCompletion:NO];
214     [crossfadeAnimation setDuration:duration];
215
216     return crossfadeAnimation;
217 }
218
219 static RetainPtr<CABasicAnimation> createShadowFadeAnimation(CFTimeInterval duration)
220 {
221     RetainPtr<CABasicAnimation> fadeShadowInAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
222     [fadeShadowInAnimation setFromValue:@0];
223     [fadeShadowInAnimation setToValue:@1];
224     [fadeShadowInAnimation setFillMode:kCAFillModeForwards];
225     [fadeShadowInAnimation setRemovedOnCompletion:NO];
226     [fadeShadowInAnimation setDuration:duration];
227
228     return fadeShadowInAnimation;
229 }
230
231 static RetainPtr<CABasicAnimation> createFadeInAnimation(CFTimeInterval duration)
232 {
233     RetainPtr<CABasicAnimation> fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
234     [fadeInAnimation setFromValue:@0];
235     [fadeInAnimation setToValue:@1];
236     [fadeInAnimation setFillMode:kCAFillModeForwards];
237     [fadeInAnimation setRemovedOnCompletion:NO];
238     [fadeInAnimation setDuration:duration];
239
240     return fadeInAnimation;
241 }
242
243 - (CFTimeInterval)_animationDuration
244 {
245     if (_textIndicator->wantsBounce()) {
246         if (_textIndicator->wantsContentCrossfade())
247             return bounceWithCrossfadeAnimationDuration;
248         return bounceAnimationDuration;
249     }
250
251     return fadeInAnimationDuration;
252 }
253
254 - (void)present
255 {
256     bool wantsBounce = _textIndicator->wantsBounce();
257     bool wantsCrossfade = _textIndicator->wantsContentCrossfade();
258     bool wantsFadeIn = _textIndicator->wantsFadeIn();
259     CFTimeInterval animationDuration = [self _animationDuration];
260
261     _hasCompletedAnimation = false;
262
263     RetainPtr<CAAnimation> presentationAnimation;
264     if (wantsBounce)
265         presentationAnimation = createBounceAnimation(animationDuration);
266     else if (wantsFadeIn)
267         presentationAnimation = createFadeInAnimation(animationDuration);
268
269     RetainPtr<CABasicAnimation> crossfadeAnimation;
270     RetainPtr<CABasicAnimation> fadeShadowInAnimation;
271     if (wantsCrossfade) {
272         crossfadeAnimation = createContentCrossfadeAnimation(animationDuration, *_textIndicator);
273         fadeShadowInAnimation = createShadowFadeAnimation(animationDuration);
274     }
275
276     [CATransaction begin];
277     for (CALayer *bounceLayer in _bounceLayers.get()) {
278         if (_textIndicator->wantsManualAnimation())
279             bounceLayer.speed = 0;
280
281         if (!wantsFadeIn)
282             bounceLayer.opacity = 1;
283
284         if (presentationAnimation)
285             [bounceLayer addAnimation:presentationAnimation.get() forKey:@"presentation"];
286
287         if (wantsCrossfade) {
288             [[bounceLayer valueForKey:textLayerKey] addAnimation:crossfadeAnimation.get() forKey:@"contentTransition"];
289             [[bounceLayer valueForKey:dropShadowLayerKey] addAnimation:fadeShadowInAnimation.get() forKey:@"fadeShadowIn"];
290             [[bounceLayer valueForKey:rimShadowLayerKey] addAnimation:fadeShadowInAnimation.get() forKey:@"fadeShadowIn"];
291         }
292     }
293     [CATransaction commit];
294 }
295
296 - (void)hideWithCompletionHandler:(void(^)(void))completionHandler
297 {
298     RetainPtr<CABasicAnimation> fadeAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
299     [fadeAnimation setFromValue:@1];
300     [fadeAnimation setToValue:@0];
301     [fadeAnimation setFillMode:kCAFillModeForwards];
302     [fadeAnimation setRemovedOnCompletion:NO];
303     [fadeAnimation setDuration:fadeOutAnimationDuration];
304
305     [CATransaction begin];
306     [CATransaction setCompletionBlock:completionHandler];
307     [self.layer addAnimation:fadeAnimation.get() forKey:@"fadeOut"];
308     [CATransaction commit];
309 }
310
311 - (void)setAnimationProgress:(float)progress
312 {
313     if (_hasCompletedAnimation)
314         return;
315
316     if (progress == 1) {
317         _hasCompletedAnimation = true;
318
319         for (CALayer *bounceLayer in _bounceLayers.get()) {
320             // Continue the animation from wherever it had manually progressed to.
321             CFTimeInterval beginTime = bounceLayer.timeOffset;
322             bounceLayer.speed = 1;
323             beginTime = [bounceLayer convertTime:CACurrentMediaTime() fromLayer:nil] - beginTime;
324             bounceLayer.beginTime = beginTime;
325         }
326     } else {
327         CFTimeInterval animationDuration = [self _animationDuration];
328         for (CALayer *bounceLayer in _bounceLayers.get())
329             bounceLayer.timeOffset = progress * animationDuration;
330     }
331 }
332
333 - (BOOL)isFlipped
334 {
335     return YES;
336 }
337
338 @end
339
340 namespace WebCore {
341
342 TextIndicatorWindow::TextIndicatorWindow(NSView *targetView)
343     : m_targetView(targetView)
344     , m_startFadeOutTimer(RunLoop::main(), this, &TextIndicatorWindow::startFadeOut)
345 {
346 }
347
348 TextIndicatorWindow::~TextIndicatorWindow()
349 {
350     switch (m_textIndicator->presentationTransition()) {
351     case TextIndicatorPresentationTransition::Crossfade:
352     case TextIndicatorPresentationTransition::FadeIn:
353         startFadeOut();
354         return;
355
356     case TextIndicatorPresentationTransition::Bounce:
357     case TextIndicatorPresentationTransition::BounceAndCrossfade:
358     case TextIndicatorPresentationTransition::None:
359         break;
360     }
361
362     closeWindow();
363 }
364
365 void TextIndicatorWindow::setAnimationProgress(float progress)
366 {
367     if (!m_textIndicator)
368         return;
369
370     [m_textIndicatorView setAnimationProgress:progress];
371 }
372
373 void TextIndicatorWindow::setTextIndicator(PassRefPtr<TextIndicator> textIndicator, CGRect contentRect, bool fadeOut)
374 {
375     if (m_textIndicator == textIndicator)
376         return;
377
378     m_textIndicator = textIndicator;
379
380     // Get rid of the old window.
381     closeWindow();
382
383     if (!m_textIndicator)
384         return;
385
386     CGFloat horizontalMargin = dropShadowBlurRadius * 2 + horizontalBorder;
387     CGFloat verticalMargin = dropShadowBlurRadius * 2 + verticalBorder;
388     
389     if (m_textIndicator->wantsBounce()) {
390         horizontalMargin = std::max(horizontalMargin, contentRect.size.width * (midBounceScale - 1) + horizontalMargin);
391         verticalMargin = std::max(verticalMargin, contentRect.size.height * (midBounceScale - 1) + verticalMargin);
392     }
393
394     contentRect = CGRectInset(contentRect, -horizontalMargin, -verticalMargin);
395     NSRect windowContentRect = [NSWindow contentRectForFrameRect:NSIntegralRect(NSRectFromCGRect(contentRect)) styleMask:NSBorderlessWindowMask];
396     m_textIndicatorWindow = adoptNS([[NSWindow alloc] initWithContentRect:windowContentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]);
397
398     [m_textIndicatorWindow setBackgroundColor:[NSColor clearColor]];
399     [m_textIndicatorWindow setOpaque:NO];
400     [m_textIndicatorWindow setIgnoresMouseEvents:YES];
401
402     m_textIndicatorView = adoptNS([[WebTextIndicatorView alloc] initWithFrame:NSMakeRect(0, 0, [m_textIndicatorWindow frame].size.width, [m_textIndicatorWindow frame].size.height) textIndicator:m_textIndicator margin:NSMakeSize(horizontalMargin, verticalMargin)]);
403     [m_textIndicatorWindow setContentView:m_textIndicatorView.get()];
404
405     [[m_targetView window] addChildWindow:m_textIndicatorWindow.get() ordered:NSWindowAbove];
406     [m_textIndicatorWindow setReleasedWhenClosed:NO];
407
408     if (m_textIndicator->presentationTransition() != TextIndicatorPresentationTransition::None)
409         [m_textIndicatorView present];
410
411     if (fadeOut)
412         m_startFadeOutTimer.startOneShot(timeBeforeFadeStarts);
413 }
414
415 void TextIndicatorWindow::closeWindow()
416 {
417     if (!m_textIndicatorWindow)
418         return;
419
420     m_startFadeOutTimer.stop();
421
422     [[m_textIndicatorWindow parentWindow] removeChildWindow:m_textIndicatorWindow.get()];
423     [m_textIndicatorWindow close];
424     m_textIndicatorWindow = nullptr;
425 }
426
427 void TextIndicatorWindow::startFadeOut()
428 {
429     RetainPtr<NSWindow> indicatorWindow = m_textIndicatorWindow;
430     [m_textIndicatorView hideWithCompletionHandler:[indicatorWindow] {
431         [[indicatorWindow parentWindow] removeChildWindow:indicatorWindow.get()];
432         [indicatorWindow close];
433     }];
434 }
435
436 } // namespace WebCore
437
438 #endif // PLATFORM(MAC)