075c082f390630fe5d12c9b1bbfeff23f841d279
[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 - (BOOL)hasCompletedAnimation;
83
84 @end
85
86 @implementation WebTextIndicatorView
87
88 - (instancetype)initWithFrame:(NSRect)frame textIndicator:(PassRefPtr<TextIndicator>)textIndicator margin:(NSSize)margin
89 {
90     if (!(self = [super initWithFrame:frame]))
91         return nil;
92
93     _textIndicator = textIndicator;
94     _margin = margin;
95
96     self.wantsLayer = YES;
97     self.layer.anchorPoint = CGPointZero;
98
99     bool wantsCrossfade = _textIndicator->wantsContentCrossfade();
100
101     FloatSize contentsImageLogicalSize = _textIndicator->contentImage()->size();
102     contentsImageLogicalSize.scale(1 / _textIndicator->contentImageScaleFactor());
103     RetainPtr<CGImageRef> contentsImage;
104     if (wantsCrossfade)
105         contentsImage = _textIndicator->contentImageWithHighlight()->getCGImageRef();
106     else
107         contentsImage = _textIndicator->contentImage()->getCGImageRef();
108
109     RetainPtr<NSMutableArray> bounceLayers = adoptNS([[NSMutableArray alloc] init]);
110
111     RetainPtr<CGColorRef> highlightColor = [NSColor colorWithDeviceRed:1 green:1 blue:0 alpha:1].CGColor;
112     RetainPtr<CGColorRef> rimShadowColor = [NSColor colorWithDeviceWhite:0 alpha:0.35].CGColor;
113     RetainPtr<CGColorRef> dropShadowColor = [NSColor colorWithDeviceWhite:0 alpha:0.2].CGColor;
114
115     RetainPtr<CGColorRef> borderColor = [NSColor colorWithDeviceRed:.96 green:.90 blue:0 alpha:1].CGColor;
116     RetainPtr<CGColorRef> gradientDarkColor = [NSColor colorWithDeviceRed:.929 green:.8 blue:0 alpha:1].CGColor;
117     RetainPtr<CGColorRef> gradientLightColor = [NSColor colorWithDeviceRed:.949 green:.937 blue:0 alpha:1].CGColor;
118
119     for (auto& textRect : _textIndicator->textRectsInBoundingRectCoordinates()) {
120         FloatRect bounceLayerRect = textRect;
121         bounceLayerRect.move(_margin.width, _margin.height);
122         bounceLayerRect.inflateX(horizontalBorder);
123         bounceLayerRect.inflateY(verticalBorder);
124
125         RetainPtr<CALayer> bounceLayer = adoptNS([[CALayer alloc] init]);
126         [bounceLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
127         [bounceLayer setFrame:bounceLayerRect];
128         [bounceLayer setOpacity:0];
129         [bounceLayers addObject:bounceLayer.get()];
130
131         FloatRect yellowHighlightRect(FloatPoint(), bounceLayerRect.size());
132         // FIXME (138888): Ideally we wouldn't remove the margin in this case, but we need to
133         // ensure that the yellow highlight and contentImageWithHighlight overlap precisely.
134         if (wantsCrossfade) {
135             yellowHighlightRect.inflateX(-horizontalBorder);
136             yellowHighlightRect.inflateY(-verticalBorder);
137         }
138
139         RetainPtr<CALayer> dropShadowLayer = adoptNS([[CALayer alloc] init]);
140         [dropShadowLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
141         [dropShadowLayer setShadowColor:dropShadowColor.get()];
142         [dropShadowLayer setShadowRadius:dropShadowBlurRadius];
143         [dropShadowLayer setShadowOffset:CGSizeMake(dropShadowOffsetX, dropShadowOffsetY)];
144         [dropShadowLayer setShadowPathIsBounds:YES];
145         [dropShadowLayer setShadowOpacity:1];
146         [dropShadowLayer setFrame:yellowHighlightRect];
147         [dropShadowLayer setCornerRadius:cornerRadius];
148         [bounceLayer addSublayer:dropShadowLayer.get()];
149         [bounceLayer setValue:dropShadowLayer.get() forKey:dropShadowLayerKey];
150
151 #if !ENABLE(LEGACY_TEXT_INDICATOR_STYLE)
152         RetainPtr<CALayer> rimShadowLayer = adoptNS([[CALayer alloc] init]);
153         [rimShadowLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
154         [rimShadowLayer setFrame:yellowHighlightRect];
155         [rimShadowLayer setShadowColor:rimShadowColor.get()];
156         [rimShadowLayer setShadowRadius:rimShadowBlurRadius];
157         [rimShadowLayer setShadowPathIsBounds:YES];
158         [rimShadowLayer setShadowOffset:CGSizeZero];
159         [rimShadowLayer setShadowOpacity:1];
160         [rimShadowLayer setFrame:yellowHighlightRect];
161         [rimShadowLayer setCornerRadius:cornerRadius];
162         [bounceLayer addSublayer:rimShadowLayer.get()];
163         [bounceLayer setValue:rimShadowLayer.get() forKey:rimShadowLayerKey];
164 #endif
165
166 #if ENABLE(LEGACY_TEXT_INDICATOR_STYLE)
167         RetainPtr<CAGradientLayer> textLayer = adoptNS([[CAGradientLayer alloc] init]);
168         [textLayer setColors:@[ (id)gradientLightColor.get(), (id)gradientDarkColor.get() ]];
169 #else
170         RetainPtr<CALayer> textLayer = adoptNS([[CALayer alloc] init]);
171 #endif
172         [textLayer setBackgroundColor:highlightColor.get()];
173         [textLayer setBorderColor:borderColor.get()];
174         [textLayer setBorderWidth:borderWidth];
175         [textLayer setDelegate:[WebActionDisablingCALayerDelegate shared]];
176         [textLayer setContents:(id)contentsImage.get()];
177
178         FloatRect imageRect = textRect;
179         imageRect.move(_textIndicator->textBoundingRectInWindowCoordinates().location() - _textIndicator->selectionRectInWindowCoordinates().location());
180         [textLayer setContentsRect:CGRectMake(imageRect.x() / contentsImageLogicalSize.width(), imageRect.y() / contentsImageLogicalSize.height(), imageRect.width() / contentsImageLogicalSize.width(), imageRect.height() / contentsImageLogicalSize.height())];
181         [textLayer setContentsGravity:kCAGravityCenter];
182         [textLayer setContentsScale:_textIndicator->contentImageScaleFactor()];
183         [textLayer setFrame:yellowHighlightRect];
184         [textLayer setCornerRadius:cornerRadius];
185         [bounceLayer setValue:textLayer.get() forKey:textLayerKey];
186         [bounceLayer addSublayer:textLayer.get()];
187     }
188
189     self.layer.sublayers = bounceLayers.get();
190     _bounceLayers = bounceLayers;
191
192     return self;
193 }
194
195 static RetainPtr<CAKeyframeAnimation> createBounceAnimation(CFTimeInterval duration)
196 {
197     RetainPtr<CAKeyframeAnimation> bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];
198     [bounceAnimation setValues:@[
199         [NSValue valueWithCATransform3D:CATransform3DIdentity],
200         [NSValue valueWithCATransform3D:CATransform3DMakeScale(midBounceScale, midBounceScale, 1)],
201         [NSValue valueWithCATransform3D:CATransform3DIdentity]
202         ]];
203     [bounceAnimation setDuration:duration];
204
205     return bounceAnimation;
206 }
207
208 static RetainPtr<CABasicAnimation> createContentCrossfadeAnimation(CFTimeInterval duration, TextIndicator& textIndicator)
209 {
210     RetainPtr<CABasicAnimation> crossfadeAnimation = [CABasicAnimation animationWithKeyPath:@"contents"];
211     RetainPtr<CGImageRef> contentsImage = textIndicator.contentImage()->getCGImageRef();
212     [crossfadeAnimation setToValue:(id)contentsImage.get()];
213     [crossfadeAnimation setFillMode:kCAFillModeForwards];
214     [crossfadeAnimation setRemovedOnCompletion:NO];
215     [crossfadeAnimation setDuration:duration];
216
217     return crossfadeAnimation;
218 }
219
220 static RetainPtr<CABasicAnimation> createShadowFadeAnimation(CFTimeInterval duration)
221 {
222     RetainPtr<CABasicAnimation> fadeShadowInAnimation = [CABasicAnimation animationWithKeyPath:@"shadowOpacity"];
223     [fadeShadowInAnimation setFromValue:@0];
224     [fadeShadowInAnimation setToValue:@1];
225     [fadeShadowInAnimation setFillMode:kCAFillModeForwards];
226     [fadeShadowInAnimation setRemovedOnCompletion:NO];
227     [fadeShadowInAnimation setDuration:duration];
228
229     return fadeShadowInAnimation;
230 }
231
232 static RetainPtr<CABasicAnimation> createFadeInAnimation(CFTimeInterval duration)
233 {
234     RetainPtr<CABasicAnimation> fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
235     [fadeInAnimation setFromValue:@0];
236     [fadeInAnimation setToValue:@1];
237     [fadeInAnimation setFillMode:kCAFillModeForwards];
238     [fadeInAnimation setRemovedOnCompletion:NO];
239     [fadeInAnimation setDuration:duration];
240
241     return fadeInAnimation;
242 }
243
244 - (CFTimeInterval)_animationDuration
245 {
246     if (_textIndicator->wantsBounce()) {
247         if (_textIndicator->wantsContentCrossfade())
248             return bounceWithCrossfadeAnimationDuration;
249         return bounceAnimationDuration;
250     }
251
252     return fadeInAnimationDuration;
253 }
254
255 - (BOOL)hasCompletedAnimation
256 {
257     return _hasCompletedAnimation;
258 }
259
260 - (void)present
261 {
262     bool wantsBounce = _textIndicator->wantsBounce();
263     bool wantsCrossfade = _textIndicator->wantsContentCrossfade();
264     bool wantsFadeIn = _textIndicator->wantsFadeIn();
265     CFTimeInterval animationDuration = [self _animationDuration];
266
267     _hasCompletedAnimation = false;
268
269     RetainPtr<CAAnimation> presentationAnimation;
270     if (wantsBounce)
271         presentationAnimation = createBounceAnimation(animationDuration);
272     else if (wantsFadeIn)
273         presentationAnimation = createFadeInAnimation(animationDuration);
274
275     RetainPtr<CABasicAnimation> crossfadeAnimation;
276     RetainPtr<CABasicAnimation> fadeShadowInAnimation;
277     if (wantsCrossfade) {
278         crossfadeAnimation = createContentCrossfadeAnimation(animationDuration, *_textIndicator);
279         fadeShadowInAnimation = createShadowFadeAnimation(animationDuration);
280     }
281
282     [CATransaction begin];
283     for (CALayer *bounceLayer in _bounceLayers.get()) {
284         if (_textIndicator->wantsManualAnimation())
285             bounceLayer.speed = 0;
286
287         if (!wantsFadeIn)
288             bounceLayer.opacity = 1;
289
290         if (presentationAnimation)
291             [bounceLayer addAnimation:presentationAnimation.get() forKey:@"presentation"];
292
293         if (wantsCrossfade) {
294             [[bounceLayer valueForKey:textLayerKey] addAnimation:crossfadeAnimation.get() forKey:@"contentTransition"];
295             [[bounceLayer valueForKey:dropShadowLayerKey] addAnimation:fadeShadowInAnimation.get() forKey:@"fadeShadowIn"];
296             [[bounceLayer valueForKey:rimShadowLayerKey] addAnimation:fadeShadowInAnimation.get() forKey:@"fadeShadowIn"];
297         }
298     }
299     [CATransaction commit];
300 }
301
302 - (void)hideWithCompletionHandler:(void(^)(void))completionHandler
303 {
304     RetainPtr<CABasicAnimation> fadeAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
305     [fadeAnimation setFromValue:@1];
306     [fadeAnimation setToValue:@0];
307     [fadeAnimation setFillMode:kCAFillModeForwards];
308     [fadeAnimation setRemovedOnCompletion:NO];
309     [fadeAnimation setDuration:fadeOutAnimationDuration];
310
311     [CATransaction begin];
312     [CATransaction setCompletionBlock:completionHandler];
313     [self.layer addAnimation:fadeAnimation.get() forKey:@"fadeOut"];
314     [CATransaction commit];
315 }
316
317 - (void)setAnimationProgress:(float)progress
318 {
319     if (_hasCompletedAnimation)
320         return;
321
322     if (progress == 1) {
323         _hasCompletedAnimation = true;
324
325         for (CALayer *bounceLayer in _bounceLayers.get()) {
326             // Continue the animation from wherever it had manually progressed to.
327             CFTimeInterval beginTime = bounceLayer.timeOffset;
328             bounceLayer.speed = 1;
329             beginTime = [bounceLayer convertTime:CACurrentMediaTime() fromLayer:nil] - beginTime;
330             bounceLayer.beginTime = beginTime;
331         }
332     } else {
333         CFTimeInterval animationDuration = [self _animationDuration];
334         for (CALayer *bounceLayer in _bounceLayers.get())
335             bounceLayer.timeOffset = progress * animationDuration;
336     }
337 }
338
339 - (BOOL)isFlipped
340 {
341     return YES;
342 }
343
344 @end
345
346 namespace WebCore {
347
348 TextIndicatorWindow::TextIndicatorWindow(NSView *targetView)
349     : m_targetView(targetView)
350     , m_startFadeOutTimer(RunLoop::main(), this, &TextIndicatorWindow::startFadeOut)
351 {
352 }
353
354 TextIndicatorWindow::~TextIndicatorWindow()
355 {
356     if (m_textIndicator->wantsManualAnimation() && [m_textIndicatorView hasCompletedAnimation]) {
357         startFadeOut();
358         return;
359     }
360
361     closeWindow();
362 }
363
364 void TextIndicatorWindow::setAnimationProgress(float progress)
365 {
366     if (!m_textIndicator)
367         return;
368
369     [m_textIndicatorView setAnimationProgress:progress];
370 }
371
372 void TextIndicatorWindow::setTextIndicator(PassRefPtr<TextIndicator> textIndicator, CGRect contentRect, bool fadeOut)
373 {
374     if (m_textIndicator == textIndicator)
375         return;
376
377     m_textIndicator = textIndicator;
378
379     // Get rid of the old window.
380     closeWindow();
381
382     if (!m_textIndicator)
383         return;
384
385     CGFloat horizontalMargin = dropShadowBlurRadius * 2 + horizontalBorder;
386     CGFloat verticalMargin = dropShadowBlurRadius * 2 + verticalBorder;
387     
388     if (m_textIndicator->wantsBounce()) {
389         horizontalMargin = std::max(horizontalMargin, contentRect.size.width * (midBounceScale - 1) + horizontalMargin);
390         verticalMargin = std::max(verticalMargin, contentRect.size.height * (midBounceScale - 1) + verticalMargin);
391     }
392
393     contentRect = CGRectInset(contentRect, -horizontalMargin, -verticalMargin);
394     NSRect windowContentRect = [NSWindow contentRectForFrameRect:NSIntegralRect(NSRectFromCGRect(contentRect)) styleMask:NSBorderlessWindowMask];
395     m_textIndicatorWindow = adoptNS([[NSWindow alloc] initWithContentRect:windowContentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]);
396
397     [m_textIndicatorWindow setBackgroundColor:[NSColor clearColor]];
398     [m_textIndicatorWindow setOpaque:NO];
399     [m_textIndicatorWindow setIgnoresMouseEvents:YES];
400
401     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)]);
402     [m_textIndicatorWindow setContentView:m_textIndicatorView.get()];
403
404     [[m_targetView window] addChildWindow:m_textIndicatorWindow.get() ordered:NSWindowAbove];
405     [m_textIndicatorWindow setReleasedWhenClosed:NO];
406
407     if (m_textIndicator->presentationTransition() != TextIndicatorPresentationTransition::None)
408         [m_textIndicatorView present];
409
410     if (fadeOut)
411         m_startFadeOutTimer.startOneShot(timeBeforeFadeStarts);
412 }
413
414 void TextIndicatorWindow::closeWindow()
415 {
416     if (!m_textIndicatorWindow)
417         return;
418
419     m_startFadeOutTimer.stop();
420
421     [[m_textIndicatorWindow parentWindow] removeChildWindow:m_textIndicatorWindow.get()];
422     [m_textIndicatorWindow close];
423     m_textIndicatorWindow = nullptr;
424 }
425
426 void TextIndicatorWindow::startFadeOut()
427 {
428     RetainPtr<NSWindow> indicatorWindow = m_textIndicatorWindow;
429     [m_textIndicatorView hideWithCompletionHandler:[indicatorWindow] {
430         [[indicatorWindow parentWindow] removeChildWindow:indicatorWindow.get()];
431         [indicatorWindow close];
432     }];
433 }
434
435 } // namespace WebCore
436
437 #endif // PLATFORM(MAC)