Add a fade transition to services highlights
authortimothy_horton@apple.com <timothy_horton@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 12 Aug 2014 19:31:04 +0000 (19:31 +0000)
committertimothy_horton@apple.com <timothy_horton@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 12 Aug 2014 19:31:04 +0000 (19:31 +0000)
https://bugs.webkit.org/show_bug.cgi?id=135829
<rdar://problem/17935736>

Reviewed by Enrica Casucci.

Add a smooth fade to highlight installation and uninstallation.
To do so, we make each highlight paint into its own small layer.

* WebProcess/WebPage/PageOverlay.cpp:
(WebKit::PageOverlay::layer):
* WebProcess/WebPage/PageOverlay.h:
* WebProcess/WebPage/PageOverlayController.cpp:
(WebKit::PageOverlayController::layerForOverlay):
* WebProcess/WebPage/PageOverlayController.h:
Expose the GraphicsLayer on PageOverlay.

* WebProcess/WebPage/ServicesOverlayController.h:
(WebKit::ServicesOverlayController::Highlight::layer):
(WebKit::ServicesOverlayController::activeHighlight):
(WebKit::ServicesOverlayController::webPage):
(WebKit::ServicesOverlayController::Highlight::Highlight): Deleted.

* WebProcess/WebPage/mac/ServicesOverlayController.mm:
(WebKit::ServicesOverlayController::Highlight::createForSelection):
(WebKit::ServicesOverlayController::Highlight::createForTelephoneNumber):
(WebKit::ServicesOverlayController::Highlight::Highlight):
Highlights now own a GraphicsLayer, which are later installed
as sublayers of the ServicesOverlayController's PageOverlay layer.
These layers are sized and positioned according to the DDHighlight's bounds.

(WebKit::ServicesOverlayController::Highlight::~Highlight):
(WebKit::ServicesOverlayController::Highlight::invalidate):
ServicesOverlayController will invalidate any remaining highlights
when it is torn down, so they can clear their backpointers.

(WebKit::ServicesOverlayController::Highlight::notifyFlushRequired):
Forward flush notifications to the DrawingArea.

(WebKit::ServicesOverlayController::Highlight::paintContents):
Paint the DDHighlight into the layer. Translation is done by the layer position,
so we zero the bounds origin when painting.

(WebKit::ServicesOverlayController::Highlight::deviceScaleFactor):
Forward the deviceScaleFactor so that things are painted at the right scale.

(WebKit::ServicesOverlayController::Highlight::fadeIn):
(WebKit::ServicesOverlayController::Highlight::fadeOut):
Apply a fade animation to the layer.

(WebKit::ServicesOverlayController::Highlight::didFinishFadeOutAnimation):
When the fade completes, unparent the layer, unless it has become active again.

(WebKit::ServicesOverlayController::ServicesOverlayController):
(WebKit::ServicesOverlayController::~ServicesOverlayController):
Invalidate all highlights, so they can clear their backpointers.

(WebKit::ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown):
Make remainingTimeUntilHighlightShouldBeShown act upon a particular highlight
instead of always the active highlight.

(WebKit::ServicesOverlayController::determineActiveHighlightTimerFired): Rename.

(WebKit::ServicesOverlayController::drawRect):
drawRect is no longer called and will no longer do anything; all of the
painting is done in sublayers.

(WebKit::ServicesOverlayController::buildPhoneNumberHighlights):
Ensure that phone number Highlights stay stable even while the selection
changes, by comparing the underlying Ranges and keeping around old Highlights
that match the new ones. This enables us to e.g. fade in while changing
the selection within a phone number.

(WebKit::ServicesOverlayController::buildSelectionHighlight):
(WebKit::ServicesOverlayController::didRebuildPotentialHighlights):
(WebKit::ServicesOverlayController::createOverlayIfNeeded):
Don't call setNeedsDisplay; the overlay doesn't have backing store.
Instead, call determineActiveHighlight, which will install/uninstall
highlights as necessary.

(WebKit::ServicesOverlayController::determineActiveHighlight):
Apply fade in/fade out to the overlays.
Keep track of which highlight we're going to activate, until the hysteresis
delay is up, then actually make it active/parent it/fade it in.
We now will have no active highlight between the fade out of the previous one
and the fade in of the new one (during the hysteresis delay).

(WebKit::ServicesOverlayController::mouseEvent):
The overlay now will not become active until the delay is up, so we don't
need to check it again here.

(WebKit::ServicesOverlayController::handleClick):
(WebKit::ServicesOverlayController::didCreateHighlight):
(WebKit::ServicesOverlayController::willDestroyHighlight):
(WebKit::ServicesOverlayController::repaintHighlightTimerFired): Deleted.
(WebKit::ServicesOverlayController::drawHighlight): Deleted.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@172483 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Source/WebKit2/ChangeLog
Source/WebKit2/WebProcess/WebPage/PageOverlay.cpp
Source/WebKit2/WebProcess/WebPage/PageOverlay.h
Source/WebKit2/WebProcess/WebPage/PageOverlayController.cpp
Source/WebKit2/WebProcess/WebPage/PageOverlayController.h
Source/WebKit2/WebProcess/WebPage/ServicesOverlayController.h
Source/WebKit2/WebProcess/WebPage/mac/ServicesOverlayController.mm

index 49b83499b2e484e069caa1bb2535a5b436d33d6a..6ff0b28baa7742bbd698662070465dc029c2f98d 100644 (file)
@@ -1,3 +1,102 @@
+2014-08-12  Tim Horton  <timothy_horton@apple.com>
+
+        Add a fade transition to services highlights
+        https://bugs.webkit.org/show_bug.cgi?id=135829
+        <rdar://problem/17935736>
+
+        Reviewed by Enrica Casucci.
+
+        Add a smooth fade to highlight installation and uninstallation.
+        To do so, we make each highlight paint into its own small layer.
+
+        * WebProcess/WebPage/PageOverlay.cpp:
+        (WebKit::PageOverlay::layer):
+        * WebProcess/WebPage/PageOverlay.h:
+        * WebProcess/WebPage/PageOverlayController.cpp:
+        (WebKit::PageOverlayController::layerForOverlay):
+        * WebProcess/WebPage/PageOverlayController.h:
+        Expose the GraphicsLayer on PageOverlay.
+
+        * WebProcess/WebPage/ServicesOverlayController.h:
+        (WebKit::ServicesOverlayController::Highlight::layer):
+        (WebKit::ServicesOverlayController::activeHighlight):
+        (WebKit::ServicesOverlayController::webPage):
+        (WebKit::ServicesOverlayController::Highlight::Highlight): Deleted.
+
+        * WebProcess/WebPage/mac/ServicesOverlayController.mm:
+        (WebKit::ServicesOverlayController::Highlight::createForSelection):
+        (WebKit::ServicesOverlayController::Highlight::createForTelephoneNumber):
+        (WebKit::ServicesOverlayController::Highlight::Highlight):
+        Highlights now own a GraphicsLayer, which are later installed
+        as sublayers of the ServicesOverlayController's PageOverlay layer.
+        These layers are sized and positioned according to the DDHighlight's bounds.
+
+        (WebKit::ServicesOverlayController::Highlight::~Highlight):
+        (WebKit::ServicesOverlayController::Highlight::invalidate):
+        ServicesOverlayController will invalidate any remaining highlights
+        when it is torn down, so they can clear their backpointers.
+
+        (WebKit::ServicesOverlayController::Highlight::notifyFlushRequired):
+        Forward flush notifications to the DrawingArea.
+
+        (WebKit::ServicesOverlayController::Highlight::paintContents):
+        Paint the DDHighlight into the layer. Translation is done by the layer position,
+        so we zero the bounds origin when painting.
+
+        (WebKit::ServicesOverlayController::Highlight::deviceScaleFactor):
+        Forward the deviceScaleFactor so that things are painted at the right scale.
+
+        (WebKit::ServicesOverlayController::Highlight::fadeIn):
+        (WebKit::ServicesOverlayController::Highlight::fadeOut):
+        Apply a fade animation to the layer.
+
+        (WebKit::ServicesOverlayController::Highlight::didFinishFadeOutAnimation):
+        When the fade completes, unparent the layer, unless it has become active again.
+
+        (WebKit::ServicesOverlayController::ServicesOverlayController):
+        (WebKit::ServicesOverlayController::~ServicesOverlayController):
+        Invalidate all highlights, so they can clear their backpointers.
+
+        (WebKit::ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown):
+        Make remainingTimeUntilHighlightShouldBeShown act upon a particular highlight
+        instead of always the active highlight.
+
+        (WebKit::ServicesOverlayController::determineActiveHighlightTimerFired): Rename.
+
+        (WebKit::ServicesOverlayController::drawRect):
+        drawRect is no longer called and will no longer do anything; all of the
+        painting is done in sublayers.
+
+        (WebKit::ServicesOverlayController::buildPhoneNumberHighlights):
+        Ensure that phone number Highlights stay stable even while the selection
+        changes, by comparing the underlying Ranges and keeping around old Highlights
+        that match the new ones. This enables us to e.g. fade in while changing
+        the selection within a phone number.
+
+        (WebKit::ServicesOverlayController::buildSelectionHighlight):
+        (WebKit::ServicesOverlayController::didRebuildPotentialHighlights):
+        (WebKit::ServicesOverlayController::createOverlayIfNeeded):
+        Don't call setNeedsDisplay; the overlay doesn't have backing store.
+        Instead, call determineActiveHighlight, which will install/uninstall
+        highlights as necessary.
+
+        (WebKit::ServicesOverlayController::determineActiveHighlight):
+        Apply fade in/fade out to the overlays.
+        Keep track of which highlight we're going to activate, until the hysteresis
+        delay is up, then actually make it active/parent it/fade it in.
+        We now will have no active highlight between the fade out of the previous one
+        and the fade in of the new one (during the hysteresis delay).
+
+        (WebKit::ServicesOverlayController::mouseEvent):
+        The overlay now will not become active until the delay is up, so we don't
+        need to check it again here.
+
+        (WebKit::ServicesOverlayController::handleClick):
+        (WebKit::ServicesOverlayController::didCreateHighlight):
+        (WebKit::ServicesOverlayController::willDestroyHighlight):
+        (WebKit::ServicesOverlayController::repaintHighlightTimerFired): Deleted.
+        (WebKit::ServicesOverlayController::drawHighlight): Deleted.
+
 2014-08-11  Andy Estes  <aestes@apple.com>
 
         [iOS] Get rid of iOS.xcconfig
index 9f96003e840eb14944c7d91caf5f88fe6dd35e47..588a3549bf91bd013b441227049975dc3494319f 100644 (file)
@@ -233,4 +233,9 @@ void PageOverlay::clear()
     m_webPage->pageOverlayController().clearPageOverlay(*this);
 }
 
+WebCore::GraphicsLayer* PageOverlay::layer()
+{
+    return m_webPage->pageOverlayController().layerForOverlay(*this);
+}
+
 } // namespace WebKit
index e5082ad4d8692a4bc3a330d0b15e3be3aa8d79b4..8025edf87a880956cc925b6cf9676cd84b545dd9 100644 (file)
@@ -35,6 +35,7 @@
 
 namespace WebCore {
 class GraphicsContext;
+class GraphicsLayer;
 }
 
 namespace WebKit {
@@ -95,6 +96,8 @@ public:
 
     WebCore::RGBA32 backgroundColor() const { return m_backgroundColor; }
     void setBackgroundColor(WebCore::RGBA32);
+
+    WebCore::GraphicsLayer* layer();
     
 protected:
     explicit PageOverlay(Client*, OverlayType);
index 4f7b18aaade462a1509c5049d4157176c435281d..eb666b902a43754e100fb905edd777def4a38e93 100644 (file)
@@ -171,6 +171,12 @@ void PageOverlayController::clearPageOverlay(PageOverlay& overlay)
     m_overlayGraphicsLayers.get(&overlay)->setDrawsContent(false);
 }
 
+GraphicsLayer* PageOverlayController::layerForOverlay(PageOverlay& overlay) const
+{
+    ASSERT(m_pageOverlays.contains(&overlay));
+    return m_overlayGraphicsLayers.get(&overlay);
+}
+
 void PageOverlayController::didChangeViewSize()
 {
     for (auto& overlayAndLayer : m_overlayGraphicsLayers) {
index 4f1689b85873bcdcd8f3cb2e1ecb7ebed88289a3..8d1b5cdce2669bfafc74bd9fb8fb206f6b92d502 100644 (file)
@@ -57,6 +57,7 @@ public:
     void setPageOverlayNeedsDisplay(PageOverlay&, const WebCore::IntRect&);
     void setPageOverlayOpacity(PageOverlay&, float);
     void clearPageOverlay(PageOverlay&);
+    WebCore::GraphicsLayer* layerForOverlay(PageOverlay&) const;
 
     void didChangeViewSize();
     void didChangeDocumentSize();
index 1b73f76d2b9db25b1d19a8f21ddb55d20181aaad..00ec39942e0ce1821c4b0d3633b1dd6322a8e9cb 100644 (file)
@@ -30,6 +30,7 @@
 
 #include "PageOverlay.h"
 #include "WebFrame.h"
+#include <WebCore/GraphicsLayerClient.h>
 #include <WebCore/Range.h>
 #include <WebCore/Timer.h>
 #include <wtf/RefCounted.h>
@@ -55,14 +56,18 @@ public:
     void selectionRectsDidChange(const Vector<WebCore::LayoutRect>&, const Vector<WebCore::GapRects>&, bool isTextOnly);
 
 private:
-    class Highlight : public RefCounted<Highlight> {
+    class Highlight : public RefCounted<Highlight>, private WebCore::GraphicsLayerClient {
         WTF_MAKE_NONCOPYABLE(Highlight);
     public:
-        static PassRefPtr<Highlight> createForSelection(RetainPtr<DDHighlightRef>);
-        static PassRefPtr<Highlight> createForTelephoneNumber(RetainPtr<DDHighlightRef>, PassRefPtr<WebCore::Range>);
+        static PassRefPtr<Highlight> createForSelection(ServicesOverlayController&, RetainPtr<DDHighlightRef>);
+        static PassRefPtr<Highlight> createForTelephoneNumber(ServicesOverlayController&, RetainPtr<DDHighlightRef>, PassRefPtr<WebCore::Range>);
+        ~Highlight();
+
+        void invalidate();
 
         DDHighlightRef ddHighlight() const { return m_ddHighlight.get(); }
         WebCore::Range* range() const { return m_range.get(); }
+        WebCore::GraphicsLayer* layer() const { return m_graphicsLayer.get(); }
 
         enum class Type {
             TelephoneNumber,
@@ -70,19 +75,25 @@ private:
         };
         Type type() const { return m_type; }
 
+        void fadeIn();
+        void fadeOut();
+
     private:
-        explicit Highlight(Type type, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<WebCore::Range> range)
-            : m_ddHighlight(ddHighlight)
-            , m_range(range)
-            , m_type(type)
-        {
-            ASSERT(m_ddHighlight);
-            ASSERT(type != Type::TelephoneNumber || m_range);
-        }
+        explicit Highlight(ServicesOverlayController&, Type, RetainPtr<DDHighlightRef>, PassRefPtr<WebCore::Range>);
+
+        // GraphicsLayerClient
+        virtual void notifyAnimationStarted(const WebCore::GraphicsLayer*, double time) override { }
+        virtual void notifyFlushRequired(const WebCore::GraphicsLayer*) override;
+        virtual void paintContents(const WebCore::GraphicsLayer*, WebCore::GraphicsContext&, WebCore::GraphicsLayerPaintingPhase, const WebCore::FloatRect& inClip) override;
+        virtual float deviceScaleFactor() const override;
+
+        void didFinishFadeOutAnimation();
 
         RetainPtr<DDHighlightRef> m_ddHighlight;
         RefPtr<WebCore::Range> m_range;
+        std::unique_ptr<WebCore::GraphicsLayer> m_graphicsLayer;
         Type m_type;
+        ServicesOverlayController* m_controller;
     };
 
     // PageOverlay::Client
@@ -104,33 +115,44 @@ private:
 
     void determineActiveHighlight(bool& mouseIsOverButton);
     void clearActiveHighlight();
+    Highlight* activeHighlight() const { return m_activeHighlight.get(); }
 
     bool hasRelevantSelectionServices();
 
     bool mouseIsOverHighlight(Highlight&, bool& mouseIsOverButton) const;
-    std::chrono::milliseconds remainingTimeUntilHighlightShouldBeShown() const;
-    void repaintHighlightTimerFired(WebCore::Timer<ServicesOverlayController>&);
+    std::chrono::milliseconds remainingTimeUntilHighlightShouldBeShown(Highlight*) const;
+    void determineActiveHighlightTimerFired(WebCore::Timer<ServicesOverlayController>&);
 
     static bool highlightsAreEquivalent(const Highlight* a, const Highlight* b);
 
     Vector<RefPtr<WebCore::Range>> telephoneNumberRangesForFocusedFrame();
 
+    void didCreateHighlight(Highlight*);
+    void willDestroyHighlight(Highlight*);
+    void didFinishFadingOutHighlight(Highlight*);
+
+    WebPage& webPage() const { return m_webPage; }
+
     WebPage& m_webPage;
     PageOverlay* m_servicesOverlay;
 
     RefPtr<Highlight> m_activeHighlight;
+    RefPtr<Highlight> m_nextActiveHighlight;
     HashSet<RefPtr<Highlight>> m_potentialHighlights;
+    HashSet<RefPtr<Highlight>> m_animatingHighlights;
+
+    HashSet<Highlight*> m_highlights;
 
     Vector<WebCore::LayoutRect> m_currentSelectionRects;
     bool m_isTextOnly;
 
     std::chrono::steady_clock::time_point m_lastSelectionChangeTime;
-    std::chrono::steady_clock::time_point m_lastActiveHighlightChangeTime;
+    std::chrono::steady_clock::time_point m_nextActiveHighlightChangeTime;
 
     RefPtr<Highlight> m_currentMouseDownOnButtonHighlight;
     WebCore::IntPoint m_mousePosition;
 
-    WebCore::Timer<ServicesOverlayController> m_repaintHighlightTimer;
+    WebCore::Timer<ServicesOverlayController> m_determineActiveHighlightTimer;
 };
 
 } // namespace WebKit
index 4d8a8c0a037fc488331229dbf725ee6df05b0424..6004b8ad4f80947751dd4fe149a301762f4023bf 100644 (file)
 #import "Logging.h"
 #import "WebPage.h"
 #import "WebProcess.h"
+#import <QuartzCore/QuartzCore.h>
 #import <WebCore/Document.h>
 #import <WebCore/FloatQuad.h>
 #import <WebCore/FocusController.h>
 #import <WebCore/FrameView.h>
 #import <WebCore/GapRects.h>
 #import <WebCore/GraphicsContext.h>
+#import <WebCore/GraphicsLayer.h>
+#import <WebCore/GraphicsLayerCA.h>
 #import <WebCore/MainFrame.h>
+#import <WebCore/PlatformCAAnimationMac.h>
 #import <WebCore/SoftLinking.h>
 
 #if __has_include(<DataDetectors/DDHighlightDrawing.h>)
@@ -50,6 +54,8 @@ typedef void* DDHighlightRef;
 #import <DataDetectors/DDHighlightDrawing_Private.h>
 #endif
 
+const float highlightFadeAnimationDuration = 0.3;
+
 typedef NSUInteger DDHighlightStyle;
 static const DDHighlightStyle DDHighlightNoOutlineWithArrow = (1 << 16);
 static const DDHighlightStyle DDHighlightOutlineWithArrow = (1 << 16) | 1;
@@ -64,14 +70,121 @@ using namespace WebCore;
 
 namespace WebKit {
 
-PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForSelection(RetainPtr<DDHighlightRef> ddHighlight)
+PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForSelection(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight)
+{
+    return adoptRef(new Highlight(controller, Type::Selection, ddHighlight, nullptr));
+}
+
+PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForTelephoneNumber(ServicesOverlayController& controller, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<Range> range)
+{
+    return adoptRef(new Highlight(controller, Type::TelephoneNumber, ddHighlight, range));
+}
+
+ServicesOverlayController::Highlight::Highlight(ServicesOverlayController& controller, Type type, RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<WebCore::Range> range)
+    : m_ddHighlight(ddHighlight)
+    , m_range(range)
+    , m_type(type)
+    , m_controller(&controller)
+{
+    ASSERT(m_ddHighlight);
+    ASSERT(type != Type::TelephoneNumber || m_range);
+
+    DrawingArea* drawingArea = controller.webPage().drawingArea();
+    m_graphicsLayer = GraphicsLayer::create(drawingArea ? drawingArea->graphicsLayerFactory() : nullptr, *this);
+    m_graphicsLayer->setDrawsContent(true);
+    m_graphicsLayer->setNeedsDisplay();
+
+    CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight.get());
+    m_graphicsLayer->setPosition(FloatPoint(highlightBoundingRect.origin));
+    m_graphicsLayer->setSize(FloatSize(highlightBoundingRect.size));
+
+    // Set directly on the PlatformCALayer so that when we leave the 'from' value implicit
+    // in our animations, we get the right initial value regardless of flush timing.
+    toGraphicsLayerCA(layer())->platformCALayer()->setOpacity(0);
+
+    controller.didCreateHighlight(this);
+}
+
+ServicesOverlayController::Highlight::~Highlight()
+{
+    if (m_controller)
+        m_controller->willDestroyHighlight(this);
+}
+
+void ServicesOverlayController::Highlight::invalidate()
 {
-    return adoptRef(new Highlight(Type::Selection, ddHighlight, nullptr));
+    layer()->removeFromParent();
+    m_controller = nullptr;
 }
 
-PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForTelephoneNumber(RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<Range> range)
+void ServicesOverlayController::Highlight::notifyFlushRequired(const GraphicsLayer*)
 {
-    return adoptRef(new Highlight(Type::TelephoneNumber, ddHighlight, range));
+    if (!m_controller)
+        return;
+
+    if (DrawingArea* drawingArea = m_controller->webPage().drawingArea())
+        drawingArea->scheduleCompositingLayerFlush();
+}
+
+void ServicesOverlayController::Highlight::paintContents(const GraphicsLayer*, GraphicsContext& graphicsContext, GraphicsLayerPaintingPhase, const FloatRect& inClip)
+{
+    CGContextRef cgContext = graphicsContext.platformContext();
+
+    CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(ddHighlight(), cgContext);
+    CGRect highlightBoundingRect = DDHighlightGetBoundingRect(ddHighlight());
+    highlightBoundingRect.origin = CGPointZero;
+
+    CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
+}
+
+float ServicesOverlayController::Highlight::deviceScaleFactor() const
+{
+    if (!m_controller)
+        return 1;
+
+    return m_controller->webPage().deviceScaleFactor();
+}
+
+void ServicesOverlayController::Highlight::fadeIn()
+{
+    RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
+    [animation setDuration:highlightFadeAnimationDuration];
+    [animation setFillMode:kCAFillModeForwards];
+    [animation setRemovedOnCompletion:false];
+    [animation setToValue:@1];
+
+    RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationMac::create(animation.get());
+    toGraphicsLayerCA(layer())->platformCALayer()->addAnimationForKey("FadeHighlightIn", platformAnimation.get());
+}
+
+void ServicesOverlayController::Highlight::fadeOut()
+{
+    RetainPtr<CABasicAnimation> animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
+    [animation setDuration:highlightFadeAnimationDuration];
+    [animation setFillMode:kCAFillModeForwards];
+    [animation setRemovedOnCompletion:false];
+    [animation setToValue:@0];
+
+    RefPtr<Highlight> retainedSelf = this;
+    [CATransaction begin];
+    [CATransaction setCompletionBlock:[retainedSelf] () {
+        retainedSelf->didFinishFadeOutAnimation();
+    }];
+
+    RefPtr<PlatformCAAnimation> platformAnimation = PlatformCAAnimationMac::create(animation.get());
+    toGraphicsLayerCA(layer())->platformCALayer()->addAnimationForKey("FadeHighlightOut", platformAnimation.get());
+    [CATransaction commit];
+}
+
+void ServicesOverlayController::Highlight::didFinishFadeOutAnimation()
+{
+    if (!m_controller)
+        return;
+
+    if (m_controller->activeHighlight() == this)
+        return;
+
+    layer()->removeFromParent();
 }
 
 static IntRect textQuadsToBoundingRectForRange(Range& range)
@@ -88,12 +201,14 @@ ServicesOverlayController::ServicesOverlayController(WebPage& webPage)
     : m_webPage(webPage)
     , m_servicesOverlay(nullptr)
     , m_isTextOnly(false)
-    , m_repaintHighlightTimer(this, &ServicesOverlayController::repaintHighlightTimerFired)
+    , m_determineActiveHighlightTimer(this, &ServicesOverlayController::determineActiveHighlightTimerFired)
 {
 }
 
 ServicesOverlayController::~ServicesOverlayController()
 {
+    for (auto& highlight : m_highlights)
+        highlight->invalidate();
 }
 
 void ServicesOverlayController::pageOverlayDestroyed(PageOverlay*)
@@ -280,57 +395,33 @@ bool ServicesOverlayController::mouseIsOverHighlight(Highlight& highlight, bool&
     return hovered;
 }
 
-std::chrono::milliseconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown() const
+std::chrono::milliseconds ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown(Highlight* highlight) const
 {
+    if (!highlight)
+        return std::chrono::milliseconds::zero();
+
     // Highlight hysteresis is only for selection services, because telephone number highlights are already much more stable
     // by virtue of being expanded to include the entire telephone number.
-    if (m_activeHighlight->type() == Highlight::Type::TelephoneNumber)
+    if (highlight->type() == Highlight::Type::TelephoneNumber)
         return std::chrono::milliseconds::zero();
 
     std::chrono::steady_clock::duration minimumTimeUntilHighlightShouldBeShown = 200_ms;
 
     auto now = std::chrono::steady_clock::now();
     auto timeSinceLastSelectionChange = now - m_lastSelectionChangeTime;
-    auto timeSinceHighlightBecameActive = now - m_lastActiveHighlightChangeTime;
+    auto timeSinceHighlightBecameActive = now - m_nextActiveHighlightChangeTime;
 
     return std::chrono::duration_cast<std::chrono::milliseconds>(std::max(minimumTimeUntilHighlightShouldBeShown - timeSinceLastSelectionChange, minimumTimeUntilHighlightShouldBeShown - timeSinceHighlightBecameActive));
 }
 
-void ServicesOverlayController::repaintHighlightTimerFired(WebCore::Timer<ServicesOverlayController>&)
-{
-    if (m_servicesOverlay)
-        m_servicesOverlay->setNeedsDisplay();
-}
-
-void ServicesOverlayController::drawHighlight(Highlight& highlight, WebCore::GraphicsContext& graphicsContext)
+void ServicesOverlayController::determineActiveHighlightTimerFired(Timer<ServicesOverlayController>&)
 {
     bool mouseIsOverButton;
-    if (!mouseIsOverHighlight(highlight, mouseIsOverButton)) {
-        LOG(Services, "ServicesOverlayController::drawHighlight - Mouse is not over highlight, so drawing nothing");
-        return;
-    }
-
-    auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown();
-    if (remainingTimeUntilHighlightShouldBeShown > std::chrono::steady_clock::duration::zero()) {
-        m_repaintHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
-        return;
-    }
-
-    CGContextRef cgContext = graphicsContext.platformContext();
-    
-    CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(highlight.ddHighlight(), cgContext);
-    CGRect highlightBoundingRect = DDHighlightGetBoundingRect(highlight.ddHighlight());
-
-    CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
+    determineActiveHighlight(mouseIsOverButton);
 }
 
-void ServicesOverlayController::drawRect(PageOverlay* overlay, WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
+void ServicesOverlayController::drawRect(PageOverlay* overlay, GraphicsContext& graphicsContext, const IntRect& dirtyRect)
 {
-    bool mouseIsOverButton;
-    determineActiveHighlight(mouseIsOverButton);
-
-    if (m_activeHighlight)
-        drawHighlight(*m_activeHighlight, graphicsContext);
 }
 
 void ServicesOverlayController::clearActiveHighlight()
@@ -357,7 +448,7 @@ void ServicesOverlayController::removeAllPotentialHighlightsOfType(Highlight::Ty
 
 void ServicesOverlayController::buildPhoneNumberHighlights()
 {
-    removeAllPotentialHighlightsOfType(Highlight::Type::TelephoneNumber);
+    HashSet<RefPtr<Highlight>> newPotentialHighlights;
 
     Frame* mainFrame = m_webPage.mainFrame();
     FrameView& mainFrameView = *mainFrame->view();
@@ -381,10 +472,32 @@ void ServicesOverlayController::buildPhoneNumberHighlights()
             CGRect cgRect = rect;
             RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
 
-            m_potentialHighlights.add(Highlight::createForTelephoneNumber(ddHighlight, range));
+            newPotentialHighlights.add(Highlight::createForTelephoneNumber(*this, ddHighlight, range));
+        }
+    }
+
+    // If any old Highlights are equivalent (by Range) to a new Highlight, reuse the old
+    // one so that any metadata is retained.
+    HashSet<RefPtr<Highlight>> reusedPotentialHighlights;
+
+    for (auto& oldHighlight : m_potentialHighlights) {
+        if (oldHighlight->type() != Highlight::Type::TelephoneNumber)
+            continue;
+
+        for (auto& newHighlight : newPotentialHighlights) {
+            if (highlightsAreEquivalent(oldHighlight.get(), newHighlight.get())) {
+                reusedPotentialHighlights.add(oldHighlight);
+                newPotentialHighlights.remove(newHighlight);
+                break;
+            }
         }
     }
 
+    removeAllPotentialHighlightsOfType(Highlight::Type::TelephoneNumber);
+
+    m_potentialHighlights.add(newPotentialHighlights.begin(), newPotentialHighlights.end());
+    m_potentialHighlights.add(reusedPotentialHighlights.begin(), reusedPotentialHighlights.end());
+
     didRebuildPotentialHighlights();
 }
 
@@ -402,7 +515,7 @@ void ServicesOverlayController::buildSelectionHighlight()
         CGRect visibleRect = m_webPage.corePage()->mainFrame().view()->visibleContentRect();
         RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
         
-        m_potentialHighlights.add(Highlight::createForSelection(ddHighlight));
+        m_potentialHighlights.add(Highlight::createForSelection(*this, ddHighlight));
     }
 
     didRebuildPotentialHighlights();
@@ -425,19 +538,19 @@ void ServicesOverlayController::didRebuildPotentialHighlights()
         return;
 
     createOverlayIfNeeded();
+
+    bool mouseIsOverButton;
+    determineActiveHighlight(mouseIsOverButton);
 }
 
 void ServicesOverlayController::createOverlayIfNeeded()
 {
-    if (m_servicesOverlay) {
-        m_servicesOverlay->setNeedsDisplay();
+    if (m_servicesOverlay)
         return;
-    }
 
     RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
     m_servicesOverlay = overlay.get();
     m_webPage.installPageOverlay(overlay.release(), PageOverlay::FadeMode::DoNotFade);
-    m_servicesOverlay->setNeedsDisplay();
 }
 
 Vector<RefPtr<Range>> ServicesOverlayController::telephoneNumberRangesForFocusedFrame()
@@ -467,13 +580,13 @@ void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActive
 {
     mouseIsOverActiveHighlightButton = false;
 
-    RefPtr<Highlight> oldActiveHighlight = m_activeHighlight.release();
+    RefPtr<Highlight> newActiveHighlight;
 
     for (auto& highlight : m_potentialHighlights) {
         if (highlight->type() == Highlight::Type::Selection) {
             // If we've already found a new active highlight, and it's
             // a telephone number highlight, prefer that over this selection highlight.
-            if (m_activeHighlight && m_activeHighlight->type() == Highlight::Type::TelephoneNumber)
+            if (newActiveHighlight && newActiveHighlight->type() == Highlight::Type::TelephoneNumber)
                 continue;
 
             // If this highlight has no compatible services, it can't be active, unless we have telephone number highlights to show in the combined menu.
@@ -486,14 +599,38 @@ void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActive
         if (!mouseIsOverHighlight(*highlight, mouseIsOverButton))
             continue;
 
-        m_activeHighlight = highlight;
+        newActiveHighlight = highlight;
         mouseIsOverActiveHighlightButton = mouseIsOverButton;
     }
 
-    if (!highlightsAreEquivalent(oldActiveHighlight.get(), m_activeHighlight.get())) {
-        m_lastActiveHighlightChangeTime = std::chrono::steady_clock::now();
-        m_servicesOverlay->setNeedsDisplay();
+    if (!this->highlightsAreEquivalent(m_activeHighlight.get(), newActiveHighlight.get())) {
+        // When transitioning to a new highlight, we might end up in determineActiveHighlight multiple times
+        // before the new highlight actually becomes active. Keep track of the last next-but-not-yet-active
+        // highlight, and only reset the active highlight hysteresis when that changes.
+        if (m_nextActiveHighlight != newActiveHighlight) {
+            m_nextActiveHighlight = newActiveHighlight;
+            m_nextActiveHighlightChangeTime = std::chrono::steady_clock::now();
+        }
+
         m_currentMouseDownOnButtonHighlight = nullptr;
+
+        if (m_activeHighlight) {
+            m_activeHighlight->fadeOut();
+            m_activeHighlight = nullptr;
+        }
+
+        auto remainingTimeUntilHighlightShouldBeShown = this->remainingTimeUntilHighlightShouldBeShown(newActiveHighlight.get());
+        if (remainingTimeUntilHighlightShouldBeShown > std::chrono::steady_clock::duration::zero()) {
+            m_determineActiveHighlightTimer.startOneShot(remainingTimeUntilHighlightShouldBeShown);
+            return;
+        }
+
+        m_activeHighlight = m_nextActiveHighlight.release();
+
+        if (m_activeHighlight) {
+            m_servicesOverlay->layer()->addChild(m_activeHighlight->layer());
+            m_activeHighlight->fadeIn();
+        }
     }
 }
 
@@ -515,7 +652,7 @@ bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& ev
         RefPtr<Highlight> mouseDownHighlight = m_currentMouseDownOnButtonHighlight;
         m_currentMouseDownOnButtonHighlight = nullptr;
 
-        if (mouseIsOverActiveHighlightButton && mouseDownHighlight && remainingTimeUntilHighlightShouldBeShown() <= std::chrono::steady_clock::duration::zero()) {
+        if (mouseIsOverActiveHighlightButton && mouseDownHighlight) {
             handleClick(m_mousePosition, *mouseDownHighlight);
             return true;
         }
@@ -536,7 +673,6 @@ bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& ev
     if (event.type() == WebEvent::MouseDown) {
         if (m_activeHighlight && mouseIsOverActiveHighlightButton) {
             m_currentMouseDownOnButtonHighlight = m_activeHighlight;
-            m_servicesOverlay->setNeedsDisplay();
             return true;
         }
 
@@ -546,7 +682,7 @@ bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& ev
     return false;
 }
 
-void ServicesOverlayController::handleClick(const WebCore::IntPoint& clickPoint, Highlight& highlight)
+void ServicesOverlayController::handleClick(const IntPoint& clickPoint, Highlight& highlight)
 {
     FrameView* frameView = m_webPage.mainFrameView();
     if (!frameView)
@@ -566,6 +702,18 @@ void ServicesOverlayController::handleClick(const WebCore::IntPoint& clickPoint,
         m_webPage.handleTelephoneNumberClick(highlight.range()->text(), windowPoint);
 }
 
+void ServicesOverlayController::didCreateHighlight(Highlight* highlight)
+{
+    ASSERT(!m_highlights.contains(highlight));
+    m_highlights.add(highlight);
+}
+
+void ServicesOverlayController::willDestroyHighlight(Highlight* highlight)
+{
+    ASSERT(m_highlights.contains(highlight));
+    m_highlights.remove(highlight);
+}
+
 } // namespace WebKit
 
 #endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)