Refactor ServiceOverlayController in preparation for fading between highlights
authortimothy_horton@apple.com <timothy_horton@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 11 Aug 2014 01:23:36 +0000 (01:23 +0000)
committertimothy_horton@apple.com <timothy_horton@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 11 Aug 2014 01:23:36 +0000 (01:23 +0000)
https://bugs.webkit.org/show_bug.cgi?id=135787
<rdar://problem/17935736>

Reviewed by Brady Eidson.

Rework ServicesOverlayController so that we always keep a set of generic
"potential highlights", which are refcounted Highlight objects and
wrap a DDHighlightRef, as well as a type (Selection or TelephoneNumber),
Range (only used in the case of TelephoneNumber), and potentially more
things in the future (like, say, fade state!).

We eagerly update the list of potential highlights when the selection or set
of detected telephone numbers changes, and use this information to install
or uninstall the page overlay as needed.

When we need to recompute the "active" highlight from this set (for example,
we need to handle a mouse event or paint the highlight), we look through
the set of potential highlights and decide. This moves the "active" highlight
decision logic into one small and confined place.

* WebProcess/WebPage/ServicesOverlayController.h:
(WebKit::ServicesOverlayController::Highlight):
Add the new aforementioned refcounted Highlight class.
Rename m_lastHoveredHighlightChangeTime to m_lastActiveHighlightChangeTime.
Make m_webPage a reference.
The rest is just added/removed/adjusted functions for the refactoring.

(WebKit::TelephoneNumberData::TelephoneNumberData): Deleted.
* WebProcess/WebPage/mac/ServicesOverlayController.mm:
(WebKit::ServicesOverlayController::Highlight::createForSelection):
(WebKit::ServicesOverlayController::Highlight::createForTelephoneNumber):
Create Highlights for the two different highlight types.

(WebKit::ServicesOverlayController::ServicesOverlayController):
(WebKit::ServicesOverlayController::willMoveToWebPage):
Our WebPage pointer is always valid because it owns us; don't clear it.
We need to keep it around so that we can uninstall the overlay and
install it again later, anyway.

(WebKit::ServicesOverlayController::selectionRectsDidChange):
(WebKit::ServicesOverlayController::selectedTelephoneNumberRangesChanged):
When selection rects or detected telephone numbers change, rebuild potential highlights.
This will have the side-effect of installing the overlay if needed.

(WebKit::ServicesOverlayController::mouseIsOverHighlight):
Make this function take a Highlight instead of a DDHighlightRef.

(WebKit::ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown):
Make this function take a Highlight instead of a DDHighlightRef.

(WebKit::ServicesOverlayController::drawHighlight):
Make this function take a Highlight instead of a DDHighlightRef.
There's no reason to do the translation separately from the layer blit,
also allowing us to avoid the StateSaver.

(WebKit::ServicesOverlayController::drawRect):
drawRect now always paints the active highlight, instead of duplicating
logic about which highlight should be active.
Also, it will update the active highlight before painting.
We no longer need to re-determine whether the active highlight's phone
number range is still a valid phone number range, because we rebuild
the potential highlights whenever the set of phone number ranges changes.

(WebKit::ServicesOverlayController::clearActiveHighlight):
Mostly an adoption of new names.

(WebKit::ServicesOverlayController::removeAllPotentialHighlightsOfType):
Run through the list of potential highlights and remove any of the given type.
The two highlight building functions use this helper to clear the old ones before building.

(WebKit::ServicesOverlayController::buildPhoneNumberHighlights):
(WebKit::ServicesOverlayController::buildSelectionHighlight):
Rebuild the list of potential highlights, replacing all highlights of
the given type with new ones.

(WebKit::ServicesOverlayController::hasRelevantSelectionServices):
Factor out the code that decides whether our current selection is
viable for servicing based on whether we have plain-text and/or rich-text services.

(WebKit::ServicesOverlayController::didRebuildPotentialHighlights):
When rebuilding potential highlights, if we have no potential highlights at all,
uninstall the page overlay; we don't need mouse tracking and don't need to
paint anything. This improves memory use and compositing performance significantly,
where previously we were leaving the overlay up forever after creating it.

If we have either detected telephone numbers or relevant selection services,
create and install the overlay if it doesn't already exist.

(WebKit::ServicesOverlayController::createOverlayIfNeeded):
This just moved from elsehwere, except that it now uses FadeMode::DoNotFade.
It doesn't make sense to fade on install/uninstall (which happens even before hover)
but not on changing the active highlight; fading will be re-addressed in the next patch.

(WebKit::ServicesOverlayController::highlightsAreEquivalent):
Determine whether two highlights are equivalent. While we may have
created a new Highlight at rebuild time, if two telephone number
highlights have equivalent ranges, there's no need to 'transition' to the new one.

(WebKit::ServicesOverlayController::determineActiveHighlight):
Run through the list of services, and try to find one that is hovered.
We prefer telephone number highlights to selection highlights, and
we will never make a selection highlight active if it is both
not serviceable and there are no telephone numbers to show in the combined menu.
This is the centralized location for determination of which highlight
should be considered active. If the active highlight changed, update
the time since last change and cancel the mouse-down tracking.

(WebKit::ServicesOverlayController::mouseEvent):
Adjust some comments to be more explanatory.
A bunch of code moved out of here and into determineActiveHighlight.

(WebKit::ServicesOverlayController::handleClick):
Adjust to take a reference and use Highlight instead of DDHighlightRef.

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

Source/WebKit2/ChangeLog
Source/WebKit2/WebProcess/WebPage/ServicesOverlayController.h
Source/WebKit2/WebProcess/WebPage/mac/ServicesOverlayController.mm

index 4593c3b87afbd27bdcc0a5c328c6f24d6397c8d4..48913eb55fdb1da33b6b3efb2dcd9fce27d1f037 100644 (file)
@@ -1,3 +1,120 @@
+2014-08-10  Tim Horton  <timothy_horton@apple.com>
+
+        Refactor ServiceOverlayController in preparation for fading between highlights
+        https://bugs.webkit.org/show_bug.cgi?id=135787
+        <rdar://problem/17935736>
+
+        Reviewed by Brady Eidson.
+
+        Rework ServicesOverlayController so that we always keep a set of generic
+        "potential highlights", which are refcounted Highlight objects and
+        wrap a DDHighlightRef, as well as a type (Selection or TelephoneNumber),
+        Range (only used in the case of TelephoneNumber), and potentially more
+        things in the future (like, say, fade state!).
+
+        We eagerly update the list of potential highlights when the selection or set
+        of detected telephone numbers changes, and use this information to install
+        or uninstall the page overlay as needed.
+
+        When we need to recompute the "active" highlight from this set (for example,
+        we need to handle a mouse event or paint the highlight), we look through
+        the set of potential highlights and decide. This moves the "active" highlight
+        decision logic into one small and confined place.
+
+        * WebProcess/WebPage/ServicesOverlayController.h:
+        (WebKit::ServicesOverlayController::Highlight):
+        Add the new aforementioned refcounted Highlight class.
+        Rename m_lastHoveredHighlightChangeTime to m_lastActiveHighlightChangeTime.
+        Make m_webPage a reference.
+        The rest is just added/removed/adjusted functions for the refactoring.
+
+        (WebKit::TelephoneNumberData::TelephoneNumberData): Deleted.
+        * WebProcess/WebPage/mac/ServicesOverlayController.mm:
+        (WebKit::ServicesOverlayController::Highlight::createForSelection):
+        (WebKit::ServicesOverlayController::Highlight::createForTelephoneNumber):
+        Create Highlights for the two different highlight types.
+
+        (WebKit::ServicesOverlayController::ServicesOverlayController):
+        (WebKit::ServicesOverlayController::willMoveToWebPage):
+        Our WebPage pointer is always valid because it owns us; don't clear it.
+        We need to keep it around so that we can uninstall the overlay and
+        install it again later, anyway.
+
+        (WebKit::ServicesOverlayController::selectionRectsDidChange):
+        (WebKit::ServicesOverlayController::selectedTelephoneNumberRangesChanged):
+        When selection rects or detected telephone numbers change, rebuild potential highlights.
+        This will have the side-effect of installing the overlay if needed.
+
+        (WebKit::ServicesOverlayController::mouseIsOverHighlight):
+        Make this function take a Highlight instead of a DDHighlightRef.
+
+        (WebKit::ServicesOverlayController::remainingTimeUntilHighlightShouldBeShown):
+        Make this function take a Highlight instead of a DDHighlightRef.
+
+        (WebKit::ServicesOverlayController::drawHighlight):
+        Make this function take a Highlight instead of a DDHighlightRef.
+        There's no reason to do the translation separately from the layer blit,
+        also allowing us to avoid the StateSaver.
+
+        (WebKit::ServicesOverlayController::drawRect):
+        drawRect now always paints the active highlight, instead of duplicating
+        logic about which highlight should be active.
+        Also, it will update the active highlight before painting.
+        We no longer need to re-determine whether the active highlight's phone
+        number range is still a valid phone number range, because we rebuild
+        the potential highlights whenever the set of phone number ranges changes.
+
+        (WebKit::ServicesOverlayController::clearActiveHighlight):
+        Mostly an adoption of new names.
+
+        (WebKit::ServicesOverlayController::removeAllPotentialHighlightsOfType):
+        Run through the list of potential highlights and remove any of the given type.
+        The two highlight building functions use this helper to clear the old ones before building.
+
+        (WebKit::ServicesOverlayController::buildPhoneNumberHighlights):
+        (WebKit::ServicesOverlayController::buildSelectionHighlight):
+        Rebuild the list of potential highlights, replacing all highlights of
+        the given type with new ones.
+
+        (WebKit::ServicesOverlayController::hasRelevantSelectionServices):
+        Factor out the code that decides whether our current selection is
+        viable for servicing based on whether we have plain-text and/or rich-text services.
+
+        (WebKit::ServicesOverlayController::didRebuildPotentialHighlights):
+        When rebuilding potential highlights, if we have no potential highlights at all,
+        uninstall the page overlay; we don't need mouse tracking and don't need to
+        paint anything. This improves memory use and compositing performance significantly,
+        where previously we were leaving the overlay up forever after creating it.
+
+        If we have either detected telephone numbers or relevant selection services,
+        create and install the overlay if it doesn't already exist.
+
+        (WebKit::ServicesOverlayController::createOverlayIfNeeded):
+        This just moved from elsehwere, except that it now uses FadeMode::DoNotFade.
+        It doesn't make sense to fade on install/uninstall (which happens even before hover)
+        but not on changing the active highlight; fading will be re-addressed in the next patch.
+
+        (WebKit::ServicesOverlayController::highlightsAreEquivalent):
+        Determine whether two highlights are equivalent. While we may have
+        created a new Highlight at rebuild time, if two telephone number
+        highlights have equivalent ranges, there's no need to 'transition' to the new one.
+
+        (WebKit::ServicesOverlayController::determineActiveHighlight):
+        Run through the list of services, and try to find one that is hovered.
+        We prefer telephone number highlights to selection highlights, and
+        we will never make a selection highlight active if it is both
+        not serviceable and there are no telephone numbers to show in the combined menu.
+        This is the centralized location for determination of which highlight
+        should be considered active. If the active highlight changed, update
+        the time since last change and cancel the mouse-down tracking.
+
+        (WebKit::ServicesOverlayController::mouseEvent):
+        Adjust some comments to be more explanatory.
+        A bunch of code moved out of here and into determineActiveHighlight.
+
+        (WebKit::ServicesOverlayController::handleClick):
+        Adjust to take a reference and use Highlight instead of DDHighlightRef.
+
 2014-08-10  Timothy Hatcher  <timothy@apple.com>
 
         Web Inspector: new glyphs are visible on OS X 10.9 builds
index b4f3b1ba08efcd498aa55cc649a046d1f22d84c5..d8618be3a8389358796bfdfef54d9885c59bf200 100644 (file)
@@ -31,6 +31,7 @@
 #include "PageOverlay.h"
 #include <WebCore/Range.h>
 #include <WebCore/Timer.h>
+#include <wtf/RefCounted.h>
 
 typedef void* DDHighlightRef;
 
@@ -44,19 +45,6 @@ namespace WebKit {
 
 class WebPage;
 
-typedef void* DDHighlightRef;
-
-struct TelephoneNumberData {
-    TelephoneNumberData(RetainPtr<DDHighlightRef> highlight, PassRefPtr<WebCore::Range> range)
-        : highlight(highlight)
-        , range(range)
-    {
-    }
-
-    RetainPtr<DDHighlightRef> highlight;
-    RefPtr<WebCore::Range> range;
-};
-
 class ServicesOverlayController : private PageOverlay::Client {
 public:
     ServicesOverlayController(WebPage&);
@@ -66,49 +54,81 @@ public:
     void selectionRectsDidChange(const Vector<WebCore::LayoutRect>&, const Vector<WebCore::GapRects>&, bool isTextOnly);
 
 private:
-    void createOverlayIfNeeded();
-    void handleClick(const WebCore::IntPoint&, DDHighlightRef);
-    void clearHighlightState();
-
+    class Highlight : public RefCounted<Highlight> {
+        WTF_MAKE_NONCOPYABLE(Highlight);
+    public:
+        static PassRefPtr<Highlight> createForSelection(RetainPtr<DDHighlightRef>);
+        static PassRefPtr<Highlight> createForTelephoneNumber(RetainPtr<DDHighlightRef>, PassRefPtr<WebCore::Range>);
+
+        DDHighlightRef ddHighlight() const { return m_ddHighlight.get(); }
+        WebCore::Range* range() const { return m_range.get(); }
+
+        enum class Type {
+            TelephoneNumber,
+            Selection
+        };
+        Type type() const { return m_type; }
+
+    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);
+        }
+
+        RetainPtr<DDHighlightRef> m_ddHighlight;
+        RefPtr<WebCore::Range> m_range;
+        Type m_type;
+    };
+
+    // PageOverlay::Client
     virtual void pageOverlayDestroyed(PageOverlay*) override;
     virtual void willMoveToWebPage(PageOverlay*, WebPage*) override;
     virtual void didMoveToWebPage(PageOverlay*, WebPage*) override;
     virtual void drawRect(PageOverlay*, WebCore::GraphicsContext&, const WebCore::IntRect& dirtyRect) override;
     virtual bool mouseEvent(PageOverlay*, const WebMouseEvent&) override;
 
-    bool drawTelephoneNumberHighlightIfVisible(WebCore::GraphicsContext&, const WebCore::IntRect& dirtyRect);
-    void drawSelectionHighlight(WebCore::GraphicsContext&, const WebCore::IntRect& dirtyRect);
-    void drawHighlight(DDHighlightRef, WebCore::GraphicsContext&);
+    void createOverlayIfNeeded();
+    void handleClick(const WebCore::IntPoint&, Highlight&);
+
+    void drawHighlight(Highlight&, WebCore::GraphicsContext&);
+
+    void removeAllPotentialHighlightsOfType(Highlight::Type);
+    void buildPhoneNumberHighlights();
+    void buildSelectionHighlight();
+    void didRebuildPotentialHighlights();
 
-    void establishHoveredTelephoneHighlight(bool& mouseIsOverButton);
-    void maybeCreateSelectionHighlight();
+    void determineActiveHighlight(bool& mouseIsOverButton);
+    void clearActiveHighlight();
 
-    void clearSelectionHighlight();
-    void clearHoveredTelephoneNumberHighlight();
+    bool hasRelevantSelectionServices();
 
-    bool mouseIsOverHighlight(DDHighlightRef, bool& mouseIsOverButton) const;
+    bool mouseIsOverHighlight(Highlight&, bool& mouseIsOverButton) const;
     std::chrono::milliseconds remainingTimeUntilHighlightShouldBeShown() const;
     void repaintHighlightTimerFired(WebCore::Timer<ServicesOverlayController>&);
 
-    WebPage* m_webPage;
+    static bool highlightsAreEquivalent(const Highlight* a, const Highlight* b);
+
+    WebPage& m_webPage;
     PageOverlay* m_servicesOverlay;
-    
+
+    RefPtr<Highlight> m_activeHighlight;
+    HashSet<RefPtr<Highlight>> m_potentialHighlights;
+
+    Vector<RefPtr<WebCore::Range>> m_currentTelephoneNumberRanges;
     Vector<WebCore::LayoutRect> m_currentSelectionRects;
-    RetainPtr<DDHighlightRef> m_selectionHighlight;
     bool m_isTextOnly;
-    std::chrono::steady_clock::time_point m_lastSelectionChangeTime;
-    std::chrono::steady_clock::time_point m_lastHoveredHighlightChangeTime;
 
-    Vector<RefPtr<WebCore::Range>> m_currentTelephoneNumberRanges;
-    Vector<RetainPtr<DDHighlightRef>> m_telephoneNumberHighlights;
-    std::unique_ptr<TelephoneNumberData> m_hoveredTelephoneNumberData;
+    std::chrono::steady_clock::time_point m_lastSelectionChangeTime;
+    std::chrono::steady_clock::time_point m_lastActiveHighlightChangeTime;
 
-    RetainPtr<DDHighlightRef> m_currentHoveredHighlight;
-    RetainPtr<DDHighlightRef> m_currentMouseDownOnButtonHighlight;
+    RefPtr<Highlight> m_currentMouseDownOnButtonHighlight;
+    WebCore::IntPoint m_mousePosition;
 
     WebCore::Timer<ServicesOverlayController> m_repaintHighlightTimer;
-
-    WebCore::IntPoint m_mousePosition;
 };
 
 } // namespace WebKit
index cf8c8d72007055d0dd4c06645d1438c653d56983..8d639e2e6041c24291357d79b8c47e1c561ba5cc 100644 (file)
@@ -63,6 +63,16 @@ using namespace WebCore;
 
 namespace WebKit {
 
+PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForSelection(RetainPtr<DDHighlightRef> ddHighlight)
+{
+    return adoptRef(new Highlight(Type::Selection, ddHighlight, nullptr));
+}
+
+PassRefPtr<ServicesOverlayController::Highlight> ServicesOverlayController::Highlight::createForTelephoneNumber(RetainPtr<DDHighlightRef> ddHighlight, PassRefPtr<Range> range)
+{
+    return adoptRef(new Highlight(Type::TelephoneNumber, ddHighlight, range));
+}
+
 static IntRect textQuadsToBoundingRectForRange(Range& range)
 {
     Vector<FloatQuad> textQuads;
@@ -74,7 +84,7 @@ static IntRect textQuadsToBoundingRectForRange(Range& range)
 }
 
 ServicesOverlayController::ServicesOverlayController(WebPage& webPage)
-    : m_webPage(&webPage)
+    : m_webPage(webPage)
     , m_servicesOverlay(nullptr)
     , m_isTextOnly(false)
     , m_repaintHighlightTimer(this, &ServicesOverlayController::repaintHighlightTimerFired)
@@ -99,31 +109,12 @@ void ServicesOverlayController::willMoveToWebPage(PageOverlay*, WebPage* webPage
 
     ASSERT(m_servicesOverlay);
     m_servicesOverlay = nullptr;
-
-    ASSERT(m_webPage);
-    m_webPage = nullptr;
 }
 
 void ServicesOverlayController::didMoveToWebPage(PageOverlay*, WebPage*)
 {
 }
 
-void ServicesOverlayController::createOverlayIfNeeded()
-{
-    if (m_servicesOverlay) {
-        m_servicesOverlay->setNeedsDisplay();
-        return;
-    }
-
-    if (m_currentTelephoneNumberRanges.isEmpty() && (!WebProcess::shared().hasSelectionServices() || m_currentSelectionRects.isEmpty()))
-        return;
-
-    RefPtr<PageOverlay> overlay = PageOverlay::create(this, PageOverlay::OverlayType::Document);
-    m_servicesOverlay = overlay.get();
-    m_webPage->installPageOverlay(overlay.release(), PageOverlay::FadeMode::Fade);
-    m_servicesOverlay->setNeedsDisplay();
-}
-
 static const uint8_t AlignmentNone = 0;
 static const uint8_t AlignmentLeft = 1 << 0;
 static const uint8_t AlignmentRight = 1 << 1;
@@ -215,7 +206,6 @@ static void compactRectsWithGapRects(Vector<LayoutRect>& rects, const Vector<Gap
 void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>& rects, const Vector<GapRects>& gapRects, bool isTextOnly)
 {
 #if __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
-    clearSelectionHighlight();
     m_currentSelectionRects = rects;
     m_isTextOnly = isTextOnly;
 
@@ -228,7 +218,7 @@ void ServicesOverlayController::selectionRectsDidChange(const Vector<LayoutRect>
 
     LOG(Services, "ServicesOverlayController - Selection rects changed - Now have %lu\n", rects.size());
 
-    createOverlayIfNeeded();
+    buildSelectionHighlight();
 #else
     UNUSED_PARAM(rects);
 #endif
@@ -239,95 +229,17 @@ void ServicesOverlayController::selectedTelephoneNumberRangesChanged(const Vecto
 #if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED > 1090
     LOG(Services, "ServicesOverlayController - Telephone number ranges changed - Had %lu, now have %lu\n", m_currentTelephoneNumberRanges.size(), ranges.size());
     m_currentTelephoneNumberRanges = ranges;
-    m_telephoneNumberHighlights.clear();
-    m_telephoneNumberHighlights.resize(ranges.size());
 
-    createOverlayIfNeeded();
+    buildPhoneNumberHighlights();
 #else
     UNUSED_PARAM(ranges);
 #endif
 }
 
-void ServicesOverlayController::clearHighlightState()
-{
-    clearSelectionHighlight();
-    clearHoveredTelephoneNumberHighlight();
-
-    m_telephoneNumberHighlights.clear();
-}
-
-void ServicesOverlayController::drawRect(PageOverlay* overlay, WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
-{
-    if (m_currentSelectionRects.isEmpty() && m_currentTelephoneNumberRanges.isEmpty()) {
-        clearHighlightState();
-        return;
-    }
-
-    if (drawTelephoneNumberHighlightIfVisible(graphicsContext, dirtyRect))
-        return;
-
-    drawSelectionHighlight(graphicsContext, dirtyRect);
-}
-
-void ServicesOverlayController::drawSelectionHighlight(WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
-{
-    // It's possible to end up drawing the selection highlight before we've actually received the selection rects.
-    // If that happens we'll end up here again once we have the rects.
-    if (m_currentSelectionRects.isEmpty())
-        return;
-
-    // If there are no appropriate installed selection services and we have no phone numbers detected, then we have nothing to draw.
-    if ((!WebProcess::shared().hasSelectionServices() || (!WebProcess::shared().hasRichContentServices() && !m_isTextOnly)) && m_currentTelephoneNumberRanges.isEmpty())
-        return;
-
-    if (!m_selectionHighlight)
-        maybeCreateSelectionHighlight();
-
-    if (m_selectionHighlight)
-        drawHighlight(m_selectionHighlight.get(), graphicsContext);
-}
-
-bool ServicesOverlayController::drawTelephoneNumberHighlightIfVisible(WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
-{
-    // Make sure the hovered telephone number highlight is still hovered.
-    if (m_hoveredTelephoneNumberData) {
-        Boolean onButton;
-        if (!DDHighlightPointIsOnHighlight(m_hoveredTelephoneNumberData->highlight.get(), (CGPoint)m_mousePosition, &onButton))
-            clearHoveredTelephoneNumberHighlight();
-
-        bool foundMatchingRange = false;
-
-        // Make sure the hovered highlight still corresponds to a current telephone number range.
-        for (auto& range : m_currentTelephoneNumberRanges) {
-            if (areRangesEqual(range.get(), m_hoveredTelephoneNumberData->range.get())) {
-                foundMatchingRange = true;
-                break;
-            }
-        }
-
-        if (!foundMatchingRange)
-            clearHoveredTelephoneNumberHighlight();
-    }
-
-    // Found out which - if any - telephone number is hovered.
-    if (!m_hoveredTelephoneNumberData) {
-        bool mouseIsOverButton;
-        establishHoveredTelephoneHighlight(mouseIsOverButton);
-    }
-
-    // If a telephone number is actually hovered, draw it.
-    if (m_hoveredTelephoneNumberData) {
-        drawHighlight(m_hoveredTelephoneNumberData->highlight.get(), graphicsContext);
-        return true;
-    }
-
-    return false;
-}
-
-bool ServicesOverlayController::mouseIsOverHighlight(DDHighlightRef highlight, bool& mouseIsOverButton) const
+bool ServicesOverlayController::mouseIsOverHighlight(Highlight& highlight, bool& mouseIsOverButton) const
 {
     Boolean onButton;
-    bool hovered = DDHighlightPointIsOnHighlight(highlight, (CGPoint)m_mousePosition, &onButton);
+    bool hovered = DDHighlightPointIsOnHighlight(highlight.ddHighlight(), (CGPoint)m_mousePosition, &onButton);
     mouseIsOverButton = onButton;
     return hovered;
 }
@@ -336,16 +248,16 @@ std::chrono::milliseconds ServicesOverlayController::remainingTimeUntilHighlight
 {
     // 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_hoveredTelephoneNumberData)
+    if (m_activeHighlight->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 timeSinceMouseOverSelection = now - m_lastHoveredHighlightChangeTime;
+    auto timeSinceHighlightBecameActive = now - m_lastActiveHighlightChangeTime;
 
-    return std::chrono::duration_cast<std::chrono::milliseconds>(std::max(minimumTimeUntilHighlightShouldBeShown - timeSinceLastSelectionChange, minimumTimeUntilHighlightShouldBeShown - timeSinceMouseOverSelection));
+    return std::chrono::duration_cast<std::chrono::milliseconds>(std::max(minimumTimeUntilHighlightShouldBeShown - timeSinceLastSelectionChange, minimumTimeUntilHighlightShouldBeShown - timeSinceHighlightBecameActive));
 }
 
 void ServicesOverlayController::repaintHighlightTimerFired(WebCore::Timer<ServicesOverlayController>&)
@@ -354,10 +266,8 @@ void ServicesOverlayController::repaintHighlightTimerFired(WebCore::Timer<Servic
         m_servicesOverlay->setNeedsDisplay();
 }
 
-void ServicesOverlayController::drawHighlight(DDHighlightRef highlight, WebCore::GraphicsContext& graphicsContext)
+void ServicesOverlayController::drawHighlight(Highlight& highlight, WebCore::GraphicsContext& graphicsContext)
 {
-    ASSERT(highlight);
-
     bool mouseIsOverButton;
     if (!mouseIsOverHighlight(highlight, mouseIsOverButton)) {
         LOG(Services, "ServicesOverlayController::drawHighlight - Mouse is not over highlight, so drawing nothing");
@@ -372,86 +282,74 @@ void ServicesOverlayController::drawHighlight(DDHighlightRef highlight, WebCore:
 
     CGContextRef cgContext = graphicsContext.platformContext();
     
-    CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(highlight, cgContext);
-    CGRect highlightBoundingRect = DDHighlightGetBoundingRect(highlight);
-    
-    GraphicsContextStateSaver stateSaver(graphicsContext);
-
-    graphicsContext.translate(toFloatSize(highlightBoundingRect.origin));
+    CGLayerRef highlightLayer = DDHighlightGetLayerWithContext(highlight.ddHighlight(), cgContext);
+    CGRect highlightBoundingRect = DDHighlightGetBoundingRect(highlight.ddHighlight());
 
-    CGRect highlightDrawRect = highlightBoundingRect;
-    highlightDrawRect.origin.x = 0;
-    highlightDrawRect.origin.y = 0;
-    
-    CGContextDrawLayerInRect(cgContext, highlightDrawRect, highlightLayer);
+    CGContextDrawLayerInRect(cgContext, highlightBoundingRect, highlightLayer);
 }
 
-void ServicesOverlayController::clearSelectionHighlight()
+void ServicesOverlayController::drawRect(PageOverlay* overlay, WebCore::GraphicsContext& graphicsContext, const WebCore::IntRect& dirtyRect)
 {
-    if (!m_selectionHighlight)
-        return;
+    bool mouseIsOverButton;
+    determineActiveHighlight(mouseIsOverButton);
 
-    if (m_currentHoveredHighlight == m_selectionHighlight)
-        m_currentHoveredHighlight = nullptr;
-    if (m_currentMouseDownOnButtonHighlight == m_selectionHighlight)
-        m_currentMouseDownOnButtonHighlight = nullptr;
-    m_selectionHighlight = nullptr;
+    if (m_activeHighlight)
+        drawHighlight(*m_activeHighlight, graphicsContext);
 }
 
-void ServicesOverlayController::clearHoveredTelephoneNumberHighlight()
+void ServicesOverlayController::clearActiveHighlight()
 {
-    if (!m_hoveredTelephoneNumberData)
+    if (!m_activeHighlight)
         return;
 
-    if (m_currentHoveredHighlight == m_hoveredTelephoneNumberData->highlight)
-        m_currentHoveredHighlight = nullptr;
-    if (m_currentMouseDownOnButtonHighlight == m_hoveredTelephoneNumberData->highlight)
+    if (m_currentMouseDownOnButtonHighlight == m_activeHighlight)
         m_currentMouseDownOnButtonHighlight = nullptr;
-    m_hoveredTelephoneNumberData = nullptr;
+    m_activeHighlight = nullptr;
 }
 
-void ServicesOverlayController::establishHoveredTelephoneHighlight(bool& mouseIsOverButton)
+void ServicesOverlayController::removeAllPotentialHighlightsOfType(Highlight::Type type)
 {
-    ASSERT(m_currentTelephoneNumberRanges.size() == m_telephoneNumberHighlights.size());
+    Vector<RefPtr<Highlight>> highlightsToRemove;
+    for (auto& highlight : m_potentialHighlights) {
+        if (highlight->type() == type)
+            highlightsToRemove.append(highlight);
+    }
 
-    for (unsigned i = 0; i < m_currentTelephoneNumberRanges.size(); ++i) {
-        if (!m_telephoneNumberHighlights[i]) {
-            // FIXME: This will choke if the range wraps around the edge of the view.
-            // What should we do in that case?
-            IntRect rect = textQuadsToBoundingRectForRange(*m_currentTelephoneNumberRanges[i]);
-
-            // Convert to the main document's coordinate space.
-            // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
-            // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
-            // not equivalent to what we need when you have a topContentInset or a header banner.
-            FrameView* viewForRange = m_currentTelephoneNumberRanges[i]->ownerDocument().view();
-            if (!viewForRange)
-                continue;
-            FrameView& mainFrameView = *m_webPage->corePage()->mainFrame().view();
-            rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
+    while (!highlightsToRemove.isEmpty())
+        m_potentialHighlights.remove(highlightsToRemove.takeLast());
+}
 
-            CGRect cgRect = rect;
-            m_telephoneNumberHighlights[i] = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
-        }
+void ServicesOverlayController::buildPhoneNumberHighlights()
+{
+    removeAllPotentialHighlightsOfType(Highlight::Type::TelephoneNumber);
 
-        if (!mouseIsOverHighlight(m_telephoneNumberHighlights[i].get(), mouseIsOverButton))
+    for (unsigned i = 0; i < m_currentTelephoneNumberRanges.size(); ++i) {
+        // FIXME: This will choke if the range wraps around the edge of the view.
+        // What should we do in that case?
+        IntRect rect = textQuadsToBoundingRectForRange(*m_currentTelephoneNumberRanges[i]);
+
+        // Convert to the main document's coordinate space.
+        // FIXME: It's a little crazy to call contentsToWindow and then windowToContents in order to get the right coordinate space.
+        // We should consider adding conversion functions to ScrollView for contentsToDocument(). Right now, contentsToRootView() is
+        // not equivalent to what we need when you have a topContentInset or a header banner.
+        FrameView* viewForRange = m_currentTelephoneNumberRanges[i]->ownerDocument().view();
+        if (!viewForRange)
             continue;
+        FrameView& mainFrameView = *m_webPage.corePage()->mainFrame().view();
+        rect.setLocation(mainFrameView.windowToContents(viewForRange->contentsToWindow(rect.location())));
 
-        if (!m_hoveredTelephoneNumberData || m_hoveredTelephoneNumberData->highlight != m_telephoneNumberHighlights[i])
-            m_hoveredTelephoneNumberData = std::make_unique<TelephoneNumberData>(m_telephoneNumberHighlights[i], m_currentTelephoneNumberRanges[i]);
+        CGRect cgRect = rect;
+        RetainPtr<DDHighlightRef> ddHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, &cgRect, 1, mainFrameView.visibleContentRect(), DDHighlightOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
 
-        m_servicesOverlay->setNeedsDisplay();
-        return;
+        m_potentialHighlights.add(Highlight::createForTelephoneNumber(ddHighlight, m_currentTelephoneNumberRanges[i]));
     }
 
-    clearHoveredTelephoneNumberHighlight();
-    mouseIsOverButton = false;
+    didRebuildPotentialHighlights();
 }
 
-void ServicesOverlayController::maybeCreateSelectionHighlight()
+void ServicesOverlayController::buildSelectionHighlight()
 {
-    ASSERT(!m_selectionHighlight);
-    ASSERT(m_servicesOverlay);
+    removeAllPotentialHighlightsOfType(Highlight::Type::Selection);
 
     Vector<CGRect> cgRects;
     cgRects.reserveCapacity(m_currentSelectionRects.size());
@@ -460,105 +358,163 @@ void ServicesOverlayController::maybeCreateSelectionHighlight()
         cgRects.append((CGRect)pixelSnappedIntRect(rect));
 
     if (!cgRects.isEmpty()) {
-        CGRect visibleRect = m_webPage->corePage()->mainFrame().view()->visibleContentRect();
-        m_selectionHighlight = adoptCF(DDHighlightCreateWithRectsInVisibleRectWithStyleAndDirection(nullptr, cgRects.begin(), cgRects.size(), visibleRect, DDHighlightNoOutlineWithArrow, YES, NSWritingDirectionNatural, NO, YES));
+        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));
+    }
 
+    didRebuildPotentialHighlights();
+}
+
+bool ServicesOverlayController::hasRelevantSelectionServices()
+{
+    return (m_isTextOnly && WebProcess::shared().hasSelectionServices()) || WebProcess::shared().hasRichContentServices();
+}
+
+void ServicesOverlayController::didRebuildPotentialHighlights()
+{
+    if (m_potentialHighlights.isEmpty()) {
+        if (m_servicesOverlay)
+            m_webPage.uninstallPageOverlay(m_servicesOverlay);
+        return;
+    }
+
+    if (m_currentTelephoneNumberRanges.isEmpty() && !hasRelevantSelectionServices())
+        return;
+
+    createOverlayIfNeeded();
+}
+
+void ServicesOverlayController::createOverlayIfNeeded()
+{
+    if (m_servicesOverlay) {
         m_servicesOverlay->setNeedsDisplay();
+        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();
 }
 
-bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
+bool ServicesOverlayController::highlightsAreEquivalent(const Highlight* a, const Highlight* b)
 {
-    m_mousePosition = m_webPage->corePage()->mainFrame().view()->rootViewToContents(event.position());
+    if (a == b)
+        return true;
 
-    DDHighlightRef oldHoveredHighlight = m_currentHoveredHighlight.get();
+    if (!a || !b)
+        return false;
 
-    bool mouseIsOverButton = false;
-    establishHoveredTelephoneHighlight(mouseIsOverButton);
-    if (m_hoveredTelephoneNumberData) {
-        ASSERT(m_hoveredTelephoneNumberData->highlight);
-        m_currentHoveredHighlight = m_hoveredTelephoneNumberData->highlight;
-    } else {
-        if (!m_selectionHighlight)
-            maybeCreateSelectionHighlight();
+    if (a->type() == Highlight::Type::TelephoneNumber && b->type() == Highlight::Type::TelephoneNumber && areRangesEqual(a->range(), b->range()))
+        return true;
 
-        if (m_selectionHighlight && mouseIsOverHighlight(m_selectionHighlight.get(), mouseIsOverButton))
-            m_currentHoveredHighlight = m_selectionHighlight;
-        else
-            m_currentHoveredHighlight = nullptr;
+    return false;
+}
+
+void ServicesOverlayController::determineActiveHighlight(bool& mouseIsOverActiveHighlightButton)
+{
+    mouseIsOverActiveHighlightButton = false;
+
+    RefPtr<Highlight> oldActiveHighlight = m_activeHighlight.release();
+
+    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)
+                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.
+            if (m_currentTelephoneNumberRanges.isEmpty() && !hasRelevantSelectionServices())
+                continue;
+        }
+
+        // If this highlight isn't hovered, it can't be active.
+        bool mouseIsOverButton;
+        if (!mouseIsOverHighlight(*highlight, mouseIsOverButton))
+            continue;
+
+        m_activeHighlight = highlight;
+        mouseIsOverActiveHighlightButton = mouseIsOverButton;
     }
 
-    if (oldHoveredHighlight != m_currentHoveredHighlight) {
-        m_lastHoveredHighlightChangeTime = std::chrono::steady_clock::now();
+    if (!highlightsAreEquivalent(oldActiveHighlight.get(), m_activeHighlight.get())) {
+        m_lastActiveHighlightChangeTime = std::chrono::steady_clock::now();
         m_servicesOverlay->setNeedsDisplay();
+        m_currentMouseDownOnButtonHighlight = nullptr;
     }
+}
+
+bool ServicesOverlayController::mouseEvent(PageOverlay*, const WebMouseEvent& event)
+{
+    m_mousePosition = m_webPage.corePage()->mainFrame().view()->rootViewToContents(event.position());
+
+    bool mouseIsOverActiveHighlightButton = false;
+    determineActiveHighlight(mouseIsOverActiveHighlightButton);
 
-    // If this event has nothing to do with the left button, it clears the current mouse down tracking and we're done processing it.
+    // Cancel the potential click if any button other than the left button changes state, and ignore the event.
     if (event.button() != WebMouseEvent::LeftButton) {
         m_currentMouseDownOnButtonHighlight = nullptr;
         return false;
     }
 
-    // Check and see if the mouse went up and we have a current mouse down highlight button.
+    // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
     if (event.type() == WebEvent::MouseUp) {
-        RetainPtr<DDHighlightRef> mouseDownHighlight = WTF::move(m_currentMouseDownOnButtonHighlight);
+        RefPtr<Highlight> mouseDownHighlight = m_currentMouseDownOnButtonHighlight;
+        m_currentMouseDownOnButtonHighlight = nullptr;
 
-        // If the mouse lifted while still over the highlight button that it went down on, then that is a click.
-        if (mouseIsOverButton && mouseDownHighlight && remainingTimeUntilHighlightShouldBeShown() <= std::chrono::steady_clock::duration::zero()) {
-            handleClick(m_mousePosition, mouseDownHighlight.get());
+        if (mouseIsOverActiveHighlightButton && mouseDownHighlight && remainingTimeUntilHighlightShouldBeShown() <= std::chrono::steady_clock::duration::zero()) {
+            handleClick(m_mousePosition, *mouseDownHighlight);
             return true;
         }
         
         return false;
     }
 
-    // Check and see if the mouse moved within the confines of the DD highlight button.
+    // If the mouse moved outside of the button tracking a potential click, stop tracking the click.
     if (event.type() == WebEvent::MouseMove) {
-        // Moving with the mouse button down is okay as long as the mouse never leaves the highlight button.
-        if (m_currentMouseDownOnButtonHighlight && mouseIsOverButton)
+        if (m_currentMouseDownOnButtonHighlight && mouseIsOverActiveHighlightButton)
             return true;
 
         m_currentMouseDownOnButtonHighlight = nullptr;
         return false;
     }
 
-    // Check and see if the mouse went down over a DD highlight button.
+    // If the mouse went down over the active highlight's button, track this as a potential click.
     if (event.type() == WebEvent::MouseDown) {
-        if (m_currentHoveredHighlight && mouseIsOverButton) {
-            m_currentMouseDownOnButtonHighlight = m_currentHoveredHighlight;
+        if (m_activeHighlight && mouseIsOverActiveHighlightButton) {
+            m_currentMouseDownOnButtonHighlight = m_activeHighlight;
             m_servicesOverlay->setNeedsDisplay();
             return true;
         }
 
         return false;
     }
-        
+
     return false;
 }
 
-void ServicesOverlayController::handleClick(const WebCore::IntPoint& clickPoint, DDHighlightRef highlight)
+void ServicesOverlayController::handleClick(const WebCore::IntPoint& clickPoint, Highlight& highlight)
 {
-    ASSERT(highlight);
-
-    FrameView* frameView = m_webPage->mainFrameView();
+    FrameView* frameView = m_webPage.mainFrameView();
     if (!frameView)
         return;
 
     IntPoint windowPoint = frameView->contentsToWindow(clickPoint);
 
-    if (highlight == m_selectionHighlight) {
+    if (highlight.type() == Highlight::Type::Selection) {
         Vector<String> selectedTelephoneNumbers;
         selectedTelephoneNumbers.reserveCapacity(m_currentTelephoneNumberRanges.size());
         for (auto& range : m_currentTelephoneNumberRanges)
             selectedTelephoneNumbers.append(range->text());
 
-        m_webPage->handleSelectionServiceClick(m_webPage->corePage()->mainFrame().selection(), selectedTelephoneNumbers, windowPoint);
-    } else if (m_hoveredTelephoneNumberData && m_hoveredTelephoneNumberData->highlight == highlight)
-        m_webPage->handleTelephoneNumberClick(m_hoveredTelephoneNumberData->range->text(), windowPoint);
-    else
-        ASSERT_NOT_REACHED();
+        m_webPage.handleSelectionServiceClick(m_webPage.corePage()->mainFrame().selection(), selectedTelephoneNumbers, windowPoint);
+    } else if (highlight.type() == Highlight::Type::TelephoneNumber)
+        m_webPage.handleTelephoneNumberClick(highlight.range()->text(), windowPoint);
 }
-    
+
 } // namespace WebKit
 
 #endif // #if ENABLE(SERVICE_CONTROLS) || ENABLE(TELEPHONE_NUMBER_DETECTION) && PLATFORM(MAC)