Add a fade transition to services highlights
[WebKit-https.git] / Source / WebKit2 / WebProcess / WebPage / mac / ServicesOverlayController.mm
1 /*
2  * Copyright (C) 2014 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 "ServicesOverlayController.h"
28
29 #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)
30
31 #import "Logging.h"
32 #import "WebPage.h"
33 #import "WebProcess.h"
34 #import <QuartzCore/QuartzCore.h>
35 #import <WebCore/Document.h>
36 #import <WebCore/FloatQuad.h>
37 #import <WebCore/FocusController.h>
38 #import <WebCore/FrameView.h>
39 #import <WebCore/GapRects.h>
40 #import <WebCore/GraphicsContext.h>
41 #import <WebCore/GraphicsLayer.h>
42 #import <WebCore/GraphicsLayerCA.h>
43 #import <WebCore/MainFrame.h>
44 #import <WebCore/PlatformCAAnimationMac.h>
45 #import <WebCore/SoftLinking.h>
46
47 #if __has_include(<DataDetectors/DDHighlightDrawing.h>)
48 #import <DataDetectors/DDHighlightDrawing.h>
49 #else
50 typedef void* DDHighlightRef;
51 #endif
52
53 #if __has_include(<DataDetectors/DDHighlightDrawing_Private.h>)
54 #import <DataDetectors/DDHighlightDrawing_Private.h>
55 #endif
56
57 const float highlightFadeAnimationDuration = 0.3;
58
59 typedef NSUInteger DDHighlightStyle;
60 static const DDHighlightStyle DDHighlightNoOutlineWithArrow = (1 << 16);
61 static const DDHighlightStyle DDHighlightOutlineWithArrow = (1 << 16) | 1;
62
63 SOFT_LINK_PRIVATE_FRAMEWORK_OPTIONAL(DataDetectors)
64 SOFT_LINK(DataDetectors, DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection, DDHighlightRef, (CFAllocatorRef allocator, CGRect* rects, CFIndex count, CGRect globalVisibleRect, DDHighlightStyle style, Boolean withArrow, NSWritingDirection writingDirection, Boolean endsWithEOL, Boolean flipped), (allocator, rects, count, globalVisibleRect, style, withArrow, writingDirection, endsWithEOL, flipped))
65 SOFT_LINK(DataDetectors, DDHighlightGetLayerWithContext, CGLayerRef, (DDHighlightRef highlight, CGContextRef context), (highlight, context))
66 SOFT_LINK(DataDetectors, DDHighlightGetBoundingRect, CGRect, (DDHighlightRef highlight), (highlight))
67 SOFT_LINK(DataDetectors, DDHighlightPointIsOnHighlight, Boolean, (DDHighlightRef highlight, CGPoint point, Boolean* onButton), (highlight, point, onButton))
68
69 using namespace WebCore;
70
71 namespace WebKit {
72
73 PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForSelection(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight)
74 {
75     return adoptRef(new Highlight(controller, Type::Selection, ddHighlight, nullptr));
76 }
77
78 PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForTelephoneNumber(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<Range> range)
79 {
80     return adoptRef(new Highlight(controller, Type::TelephoneNumber, ddHighlight, range));
81 }
82
83 ServicesOverlayController::Highlight::Highlight(ServicesOverlayController& controller, Type type, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<WebCore::Range> range)
84     : m_ddHighlight(ddHighlight)
85     , m_range(range)
86     , m_type(type)
87     , m_controller(&controller)
88 {
89     ASSERT(m_ddHighlight);
90     ASSERT(type != Type::TelephoneNumber || m_range);
91
92     DrawingArea* drawingArea = controller.webPage().drawingArea();
93     m_graphicsLayer = GraphicsLayer::create(drawingArea ? drawingArea->graphicsLayerFactory() : nullptr, *this);
94     m_graphicsLayer->setDrawsContent(true);
95     m_graphicsLayer->setNeedsDisplay();
96
97     CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight.get());
98     m_graphicsLayer->setPosition(FloatPoint(highlightBoundingRect.origin));
99     m_graphicsLayer->setSize(FloatSize(highlightBoundingRect.size));
100
101     // Set directly on the PlatformCALayer so that when we leave the 'from' value implicit
102     // in our animations, we get the right initial value regardless of flush timing.
103     toGraphicsLayerCA(layer())->platformCALayer()->setOpacity(0);
104
105     controller.didCreateHighlight(this);
106 }
107
108 ServicesOverlayController::Highlight::~Highlight()
109 {
110     if (m_controller)
111         m_controller->willDestroyHighlight(this);
112 }
113
114 void ServicesOverlayController::Highlight::invalidate()
115 {
116     layer()->removeFromParent();
117     m_controller = nullptr;
118 }
119
120 void ServicesOverlayController::Highlight::notifyFlushRequired(const GraphicsLayer*)
121 {
122     if (!m_controller)
123         return;
124
125     if (DrawingArea* drawingArea = m_controller->webPage().drawingArea())
126         drawingArea->scheduleCompositingLayerFlush();
127 }
128
129 void ServicesOverlayController::Highlight::paintContents(const GraphicsLayer*, GraphicsContext& graphicsContext, GraphicsLayerPaintingPhase, const FloatRect& inClip)
130 {
131     CGContextRef cgContext = graphicsContext.platformContext();
132
133     CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(ddHighlight(), cgContext);
134     CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight());
135     highlightBoundingRect.origin = CGPointZero;
136
137     CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
138 }
139
140 float ServicesOverlayController::Highlight::deviceScaleFactor() const
141 {
142     if (!m_controller)
143         return 1;
144
145     return m_controller->webPage().deviceScaleFactor();
146 }
147
148 void ServicesOverlayController::Highlight::fadeIn()
149 {
150     RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
151     [animation setDuration:highlightFadeAnimationDuration];
152     [animation setFillMode:kCAFillModeForwards];
153     [animation setRemovedOnCompletion:false];
154     [animation setToValue:@1];
155
156     RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationMac::create(animation.get());
157     toGraphicsLayerCA(layer())->platformCALayer()->addAnimationForKey("FadeHighlightIn", platformAnimation.get());
158 }
159
160 void ServicesOverlayController::Highlight::fadeOut()
161 {
162     RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
163     [animation setDuration:highlightFadeAnimationDuration];
164     [animation setFillMode:kCAFillModeForwards];
165     [animation setRemovedOnCompletion:false];
166     [animation setToValue:@0];
167
168     RefPtr<Highlight> retainedSelf = this;
169     [CATransaction begin];
170     [CATransaction setCompletionBlock:[retainedSelf] () {
171         retainedSelf->didFinishFadeOutAnimation();
172     }];
173
174     RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationMac::create(animation.get());
175     toGraphicsLayerCA(layer())->platformCALayer()->addAnimationForKey("FadeHighlightOut", platformAnimation.get());
176     [CATransaction commit];
177 }
178
179 void ServicesOverlayController::Highlight::didFinishFadeOutAnimation()
180 {
181     if (!m_controller)
182         return;
183
184     if (m_controller->activeHighlight() == this)
185         return;
186
187     layer()->removeFromParent();
188 }
189
190 static IntRect textQuadsToBoundingRectForRange(Range& range)
191 {
192     Vector<FloatQuad> textQuads;
193     range.textQuads(textQuads);
194     FloatRect boundingRect;
195     for (auto& quad : textQuads)
196         boundingRect.unite(quad.boundingBox());
197     return enclosingIntRect(boundingRect);
198 }
199
200 ServicesOverlayController::ServicesOverlayController(WebPage& webPage)
201     : m_webPage(webPage)
202     , m_servicesOverlay(nullptr)
203     , m_isTextOnly(false)
204     , m_determineActiveHighlightTimer(this, &ServicesOverlayController::determineActiveHighlightTimerFired)
205 {
206 }
207
208 ServicesOverlayController::~ServicesOverlayController()
209 {
210     for (auto& highlight : m_highlights)
211         highlight->invalidate();
212 }
213
214 void ServicesOverlayController::pageOverlayDestroyed(PageOverlay*)
215 {
216     // Before the overlay is destroyed, it should have moved out of the WebPage,
217     // at which point we already cleared our back pointer.
218     ASSERT(!m_servicesOverlay);
219 }
220
221 void ServicesOverlayController::willMoveToWebPage(PageOverlay*, WebPage* webPage)
222 {
223     if (webPage)
224         return;
225
226     ASSERT(m_servicesOverlay);
227     m_servicesOverlay = nullptr;
228 }
229
230 void ServicesOverlayController::didMoveToWebPage(PageOverlay*, WebPage*)
231 {
232 }
233
234 static const uint8_t AlignmentNone = 0;
235 static const uint8_t AlignmentLeft = 1 << 0;
236 static const uint8_t AlignmentRight = 1 << 1;
237
238 static void expandForGap(Vector<LayoutRect>& rects, uint8_t* alignments, const GapRects& gap)
239 {
240     if (!gap.left().isEmpty()) {
241         LayoutUnit leftEdge = gap.left().x();
242         for (unsigned i = 0; i < 3; ++i) {
243             if (alignments[i] & AlignmentLeft)
244                 rects[i].shiftXEdgeTo(leftEdge);
245         }
246     }
247
248     if (!gap.right().isEmpty()) {
249         LayoutUnit rightEdge = gap.right().maxX();
250         for (unsigned i = 0; i < 3; ++i) {
251             if (alignments[i] & AlignmentRight)
252                 rects[i].shiftMaxXEdgeTo(rightEdge);
253         }
254     }
255 }
256
257 static inline void stitchRects(Vector<LayoutRect>& rects)
258 {
259     if (rects.size() <= 1)
260         return;
261     
262     Vector<LayoutRect> newRects;
263     
264     // FIXME: Need to support vertical layout.
265     // First stitch together all the rects on the first line of the selection.
266     size_t indexFromStart = 0;
267     LayoutUnit firstTop = rects[indexFromStart].y();
268     LayoutRect& currentRect = rects[indexFromStart++];
269     while (indexFromStart < rects.size() && rects[indexFromStart].y() == firstTop)
270         currentRect.unite(rects[indexFromStart++]);
271     
272     newRects.append(currentRect);
273     if (indexFromStart == rects.size()) {
274         // All the rects are on one line. There is nothing else to do.
275         rects.swap(newRects);
276         return;
277     }
278     
279     // Next stitch together all the rects on the last line of the selection.
280     size_t indexFromEnd = rects.size() - 1;
281     LayoutUnit lastTop = rects[indexFromEnd].y();
282     LayoutRect lastRect = rects[indexFromEnd];
283     while (indexFromEnd != indexFromStart && rects[--indexFromEnd].y() == lastTop)
284         lastRect.unite(rects[indexFromEnd]);
285     
286     if (indexFromEnd == indexFromStart) {
287         // All the rects are on two lines only. There is nothing else to do.
288         newRects.append(lastRect);
289         rects.swap(newRects);
290         return;
291     }
292     
293     // indexFromStart is the index of the first rectangle on the second line.
294     // indexFromEnd is the index of the last rectangle on the second to the last line.
295     // Stitch together all the rects after the first line until the second to the last included.
296     currentRect = rects[indexFromStart];
297     while (indexFromStart != indexFromEnd)
298         currentRect.unite(rects[++indexFromStart]);
299     
300     newRects.append(currentRect);
301     newRects.append(lastRect);
302
303     rects.swap(newRects);
304 }
305
306 static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects)
307 {
308     stitchRects(rects);
309     
310     // FIXME: The following alignments are correct for LTR text.
311     // We should also account for RTL.
312     uint8_t alignments[3];
313     if (rects.size() == 1) {
314         alignments[0] = AlignmentLeft | AlignmentRight;
315         alignments[1] = AlignmentNone;
316         alignments[2] = AlignmentNone;
317     } else if (rects.size() == 2) {
318         alignments[0] = AlignmentRight;
319         alignments[1] = AlignmentLeft;
320         alignments[2] = AlignmentNone;
321     } else {
322         alignments[0] = AlignmentRight;
323         alignments[1] = AlignmentLeft | AlignmentRight;
324         alignments[2] = AlignmentLeft;
325     }
326
327     // Account for each GapRects by extending the edge of certain LayoutRects to meet the gap.
328     for (auto& gap : gapRects)
329         expandForGap(rects, alignments, gap);
330
331     // If we have 3 rects we might need one final GapRects to align the edges.
332     if (rects.size() == 3) {
333         LayoutRect left;
334         LayoutRect right;
335         for (unsigned i = 0; i < 3; ++i) {
336             if (alignments[i] & AlignmentLeft) {
337                 if (left.isEmpty())
338                     left = rects[i];
339                 else if (rects[i].x() < left.x())
340                     left = rects[i];
341             }
342             if (alignments[i] & AlignmentRight) {
343                 if (right.isEmpty())
344                     right = rects[i];
345                 else if ((rects[i].x() + rects[i].width()) > (right.x() + right.width()))
346                     right = rects[i];
347             }
348         }
349
350         if (!left.isEmpty() || !right.isEmpty()) {
351             GapRects gap;
352             gap.uniteLeft(left);
353             gap.uniteRight(right);
354             expandForGap(rects, alignments, gap);
355         }
356     }
357 }
358
359 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly)
360 {
361 #if __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
362     m_currentSelectionRects = rects;
363     m_isTextOnly = isTextOnly;
364
365     m_lastSelectionChangeTime = std::chrono::steady_clock::now();
366
367     compactRectsWithGapRects(m_currentSelectionRects, gapRects);
368
369     // DataDetectors needs these reversed in order to place the arrow in the right location.
370     m_currentSelectionRects.reverse();
371
372     LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
373
374     buildSelectionHighlight();
375 #else
376     UNUSED_PARAM(rects);
377 #endif
378 }
379
380 void ServicesOverlayController::selectedTelephoneNumberRangesChanged()
381 {
382 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
383     LOG(Services, "ServicesOverlayController - Telephone number ranges changed\n");
384     buildPhoneNumberHighlights();
385 #else
386     UNUSED_PARAM(ranges);
387 #endif
388 }
389
390 bool ServicesOverlayController::mouseIsOverHighlight(Highlight& highlight, bool& mouseIsOverButton) const
391 {
392     Boolean onButton;
393     bool hovered = DDHighlightPointIsOnHighlight(highlight.ddHighlight(), (CGPoint)m_mousePosition, &onButton);
394     mouseIsOverButton = onButton;
395     return hovered;
396 }
397
398 std::chrono::milliseconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown(Highlight* highlight) const
399 {
400     if (!highlight)
401         return std::chrono::milliseconds::zero();
402
403     // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable
404     // by virtue of being expanded to include the entire telephone number.
405     if (highlight->type() == Highlight::Type::TelephoneNumber)
406         return std::chrono::milliseconds::zero();
407
408     std::chrono::steady_clock::duration minimumTimeUntilHighlightShouldBeShown = 200_ms;
409
410     auto now = std::chrono::steady_clock::now();
411     auto timeSinceLastSelectionChange = now - m_lastSelectionChangeTime;
412     auto timeSinceHighlightBecameActive = now - m_nextActiveHighlightChangeTime;
413
414     return std::chrono::duration_cast<std::chrono::milliseconds>(std::max(minimumTimeUntilHighlightShouldBeShown - timeSinceLastSelectionChange, minimumTimeUntilHighlightShouldBeShown - timeSinceHighlightBecameActive));
415 }
416
417 void ServicesOverlayController::determineActiveHighlightTimerFired(Timer<ServicesOverlayController>&)
418 {
419     bool mouseIsOverButton;
420     determineActiveHighlight(mouseIsOverButton);
421 }
422
423 void ServicesOverlayController::drawRect(PageOverlay* overlay, GraphicsContext& graphicsContext, const IntRect& dirtyRect)
424 {
425 }
426
427 void ServicesOverlayController::clearActiveHighlight()
428 {
429     if (!m_activeHighlight)
430         return;
431
432     if (m_currentMouseDownOnButtonHighlight == m_activeHighlight)
433         m_currentMouseDownOnButtonHighlight = nullptr;
434     m_activeHighlight = nullptr;
435 }
436
437 void ServicesOverlayController::removeAllPotentialHighlightsOfType(Highlight::Type type)
438 {
439     Vector<RefPtr<Highlight>> highlightsToRemove;
440     for (auto& highlight : m_potentialHighlights) {
441         if (highlight->type() == type)
442             highlightsToRemove.append(highlight);
443     }
444
445     while (!highlightsToRemove.isEmpty())
446         m_potentialHighlights.remove(highlightsToRemove.takeLast());
447 }
448
449 void ServicesOverlayController::buildPhoneNumberHighlights()
450 {
451     HashSet<RefPtr<Highlight>> newPotentialHighlights;
452
453     Frame* mainFrame = m_webPage.mainFrame();
454     FrameView& mainFrameView = *mainFrame->view();
455
456     for (Frame* frame = mainFrame; frame; frame = frame->tree().traverseNext()) {
457         auto& ranges = frame->editor().detectedTelephoneNumberRanges();
458         for (auto& range : ranges) {
459             // FIXME: This will choke if the range wraps around the edge of the view.
460             // What should we do in that case?
461             IntRect rect = textQuadsToBoundingRectForRange(*range);
462
463             // Convert to the main document's coordinate space.
464             // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
465             // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
466             // not equivalent to what we need when you have a topContentInset or a header banner.
467             FrameView* viewForRange = range->ownerDocument().view();
468             if (!viewForRange)
469                 continue;
470             rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
471
472             CGRect cgRect = rect;
473             RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
474
475             newPotentialHighlights.add(Highlight::createForTelephoneNumber(*this, ddHighlight, range));
476         }
477     }
478
479     // If any old Highlights are equivalent (by Range) to a new Highlight, reuse the old
480     // one so that any metadata is retained.
481     HashSet<RefPtr<Highlight>> reusedPotentialHighlights;
482
483     for (auto& oldHighlight : m_potentialHighlights) {
484         if (oldHighlight->type() != Highlight::Type::TelephoneNumber)
485             continue;
486
487         for (auto& newHighlight : newPotentialHighlights) {
488             if (highlightsAreEquivalent(oldHighlight.get(), newHighlight.get())) {
489                 reusedPotentialHighlights.add(oldHighlight);
490                 newPotentialHighlights.remove(newHighlight);
491                 break;
492             }
493         }
494     }
495
496     removeAllPotentialHighlightsOfType(Highlight::Type::TelephoneNumber);
497
498     m_potentialHighlights.add(newPotentialHighlights.begin(), newPotentialHighlights.end());
499     m_potentialHighlights.add(reusedPotentialHighlights.begin(), reusedPotentialHighlights.end());
500
501     didRebuildPotentialHighlights();
502 }
503
504 void ServicesOverlayController::buildSelectionHighlight()
505 {
506     removeAllPotentialHighlightsOfType(Highlight::Type::Selection);
507
508     Vector<CGRect> cgRects;
509     cgRects.reserveCapacity(m_currentSelectionRects.size());
510
511     for (auto& rect : m_currentSelectionRects)
512         cgRects.append((CGRect)pixelSnappedIntRect(rect));
513
514     if (!cgRects.isEmpty()) {
515         CGRect visibleRect = m_webPage.corePage()->mainFrame().view()->visibleContentRect();
516         RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
517         
518         m_potentialHighlights.add(Highlight::createForSelection(*this, ddHighlight));
519     }
520
521     didRebuildPotentialHighlights();
522 }
523
524 bool ServicesOverlayController::hasRelevantSelectionServices()
525 {
526     return (m_isTextOnly && WebProcess::shared().hasSelectionServices()) || WebProcess::shared().hasRichContentServices();
527 }
528
529 void ServicesOverlayController::didRebuildPotentialHighlights()
530 {
531     if (m_potentialHighlights.isEmpty()) {
532         if (m_servicesOverlay)
533             m_webPage.uninstallPageOverlay(m_servicesOverlay);
534         return;
535     }
536
537     if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices())
538         return;
539
540     createOverlayIfNeeded();
541
542     bool mouseIsOverButton;
543     determineActiveHighlight(mouseIsOverButton);
544 }
545
546 void ServicesOverlayController::createOverlayIfNeeded()
547 {
548     if (m_servicesOverlay)
549         return;
550
551     RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
552     m_servicesOverlay = overlay.get();
553     m_webPage.installPageOverlay(overlay.release(), PageOverlay::FadeMode::DoNotFade);
554 }
555
556 Vector<RefPtr<Range>> ServicesOverlayController::telephoneNumberRangesForFocusedFrame()
557 {
558     Page* page = m_webPage.corePage();
559     if (!page)
560         return Vector<RefPtr<Range>>();
561
562     return page->focusController().focusedOrMainFrame().editor().detectedTelephoneNumberRanges();
563 }
564
565 bool ServicesOverlayController::highlightsAreEquivalent(const Highlight* a, const Highlight* b)
566 {
567     if (a == b)
568         return true;
569
570     if (!a || !b)
571         return false;
572
573     if (a->type() == Highlight::Type::TelephoneNumber && b->type() == Highlight::Type::TelephoneNumber && areRangesEqual(a->range(), b->range()))
574         return true;
575
576     return false;
577 }
578
579 void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActiveHighlightButton)
580 {
581     mouseIsOverActiveHighlightButton = false;
582
583     RefPtr<Highlight> newActiveHighlight;
584
585     for (auto& highlight : m_potentialHighlights) {
586         if (highlight->type() == Highlight::Type::Selection) {
587             // If we've already found a new active highlight, and it's
588             // a telephone number highlight, prefer that over this selection highlight.
589             if (newActiveHighlight && newActiveHighlight->type() == Highlight::Type::TelephoneNumber)
590                 continue;
591
592             // If this highlight has no compatible services, it can't be active, unless we have telephone number highlights to show in the combined menu.
593             if (telephoneNumberRangesForFocusedFrame().isEmpty() && !hasRelevantSelectionServices())
594                 continue;
595         }
596
597         // If this highlight isn't hovered, it can't be active.
598         bool mouseIsOverButton;
599         if (!mouseIsOverHighlight(*highlight, mouseIsOverButton))
600             continue;
601
602         newActiveHighlight = highlight;
603         mouseIsOverActiveHighlightButton = mouseIsOverButton;
604     }
605
606     if (!this->highlightsAreEquivalent(m_activeHighlight.get(), newActiveHighlight.get())) {
607         // When transitioning to a new highlight, we might end up in determineActiveHighlight multiple times
608         // before the new highlight actually becomes active. Keep track of the last next-but-not-yet-active
609         // highlight, and only reset the active highlight hysteresis when that changes.
610         if (m_nextActiveHighlight != newActiveHighlight) {
611             m_nextActiveHighlight = newActiveHighlight;
612             m_nextActiveHighlightChangeTime = std::chrono::steady_clock::now();
613         }
614
615         m_currentMouseDownOnButtonHighlight = nullptr;
616
617         if (m_activeHighlight) {
618             m_activeHighlight->fadeOut();
619             m_activeHighlight = nullptr;
620         }
621
622         auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown(newActiveHighlight.get());
623         if (remainingTimeUntilHighlightShouldBeShown > std::chrono::steady_clock::duration::zero()) {
624             m_determineActiveHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
625             return;
626         }
627
628         m_activeHighlight = m_nextActiveHighlight.release();
629
630         if (m_activeHighlight) {
631             m_servicesOverlay->layer()->addChild(m_activeHighlight->layer());
632             m_activeHighlight->fadeIn();
633         }
634     }
635 }
636
637 bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
638 {
639     m_mousePosition = m_webPage.corePage()->mainFrame().view()->rootViewToContents(event.position());
640
641     bool mouseIsOverActiveHighlightButton = false;
642     determineActiveHighlight(mouseIsOverActiveHighlightButton);
643
644     // Cancel the potential click if any button other than the left button changes state, and ignore the event.
645     if (event.button() != WebMouseEvent::LeftButton) {
646         m_currentMouseDownOnButtonHighlight = nullptr;
647         return false;
648     }
649
650     // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
651     if (event.type() == WebEvent::MouseUp) {
652         RefPtr<Highlight> mouseDownHighlight = m_currentMouseDownOnButtonHighlight;
653         m_currentMouseDownOnButtonHighlight = nullptr;
654
655         if (mouseIsOverActiveHighlightButton && mouseDownHighlight) {
656             handleClick(m_mousePosition, *mouseDownHighlight);
657             return true;
658         }
659         
660         return false;
661     }
662
663     // If the mouse moved outside of the button tracking a potential click, stop tracking the click.
664     if (event.type() == WebEvent::MouseMove) {
665         if (m_currentMouseDownOnButtonHighlight && mouseIsOverActiveHighlightButton)
666             return true;
667
668         m_currentMouseDownOnButtonHighlight = nullptr;
669         return false;
670     }
671
672     // If the mouse went down over the active highlight's button, track this as a potential click.
673     if (event.type() == WebEvent::MouseDown) {
674         if (m_activeHighlight && mouseIsOverActiveHighlightButton) {
675             m_currentMouseDownOnButtonHighlight = m_activeHighlight;
676             return true;
677         }
678
679         return false;
680     }
681
682     return false;
683 }
684
685 void ServicesOverlayController::handleClick(const IntPoint& clickPoint, Highlight& highlight)
686 {
687     FrameView* frameView = m_webPage.mainFrameView();
688     if (!frameView)
689         return;
690
691     IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
692
693     if (highlight.type() == Highlight::Type::Selection) {
694         auto telephoneNumberRanges = telephoneNumberRangesForFocusedFrame();
695         Vector<String> selectedTelephoneNumbers;
696         selectedTelephoneNumbers.reserveCapacity(telephoneNumberRanges.size());
697         for (auto& range : telephoneNumberRanges)
698             selectedTelephoneNumbers.append(range->text());
699
700         m_webPage.handleSelectionServiceClick(m_webPage.corePage()->mainFrame().selection(), selectedTelephoneNumbers, windowPoint);
701     } else if (highlight.type() == Highlight::Type::TelephoneNumber)
702         m_webPage.handleTelephoneNumberClick(highlight.range()->text(), windowPoint);
703 }
704
705 void ServicesOverlayController::didCreateHighlight(Highlight* highlight)
706 {
707     ASSERT(!m_highlights.contains(highlight));
708     m_highlights.add(highlight);
709 }
710
711 void ServicesOverlayController::willDestroyHighlight(Highlight* highlight)
712 {
713     ASSERT(m_highlights.contains(highlight));
714     m_highlights.remove(highlight);
715 }
716
717 } // namespace WebKit
718
719 #endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)