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