| /* |
| * Copyright (C) 2014-2017 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "DebugPageOverlays.h" |
| |
| #include "ColorHash.h" |
| #include "ElementIterator.h" |
| #include "FrameView.h" |
| #include "GraphicsContext.h" |
| #include "Page.h" |
| #include "PageOverlay.h" |
| #include "PageOverlayController.h" |
| #include "Region.h" |
| #include "ScrollingCoordinator.h" |
| #include "Settings.h" |
| |
| namespace WebCore { |
| |
| DebugPageOverlays* DebugPageOverlays::sharedDebugOverlays; |
| |
| class RegionOverlay : public RefCounted<RegionOverlay>, public PageOverlay::Client { |
| public: |
| static Ref<RegionOverlay> create(Page&, DebugPageOverlays::RegionType); |
| virtual ~RegionOverlay(); |
| |
| void recomputeRegion(); |
| PageOverlay& overlay() { return *m_overlay; } |
| |
| protected: |
| RegionOverlay(Page&, Color); |
| |
| private: |
| void willMoveToPage(PageOverlay&, Page*) final; |
| void didMoveToPage(PageOverlay&, Page*) final; |
| void drawRect(PageOverlay&, GraphicsContext&, const IntRect& dirtyRect) override; |
| bool mouseEvent(PageOverlay&, const PlatformMouseEvent&) final; |
| void didScrollFrame(PageOverlay&, Frame&) final; |
| |
| protected: |
| // Returns true if the region changed. |
| virtual bool updateRegion() = 0; |
| void drawRegion(GraphicsContext&, const Region&, const Color&, const IntRect& dirtyRect); |
| |
| Page& m_page; |
| RefPtr<PageOverlay> m_overlay; |
| std::unique_ptr<Region> m_region; |
| Color m_color; |
| }; |
| |
| class MouseWheelRegionOverlay final : public RegionOverlay { |
| public: |
| static Ref<MouseWheelRegionOverlay> create(Page& page) |
| { |
| return adoptRef(*new MouseWheelRegionOverlay(page)); |
| } |
| |
| private: |
| explicit MouseWheelRegionOverlay(Page& page) |
| : RegionOverlay(page, Color(0.5f, 0.0f, 0.0f, 0.4f)) |
| { |
| } |
| |
| bool updateRegion() override; |
| }; |
| |
| bool MouseWheelRegionOverlay::updateRegion() |
| { |
| auto region = makeUnique<Region>(); |
| |
| for (const Frame* frame = &m_page.mainFrame(); frame; frame = frame->tree().traverseNext()) { |
| if (!frame->view() || !frame->document()) |
| continue; |
| |
| auto frameRegion = frame->document()->absoluteRegionForEventTargets(frame->document()->wheelEventTargets()); |
| frameRegion.first.translate(toIntSize(frame->view()->contentsToRootView(IntPoint()))); |
| region->unite(frameRegion.first); |
| } |
| |
| region->translate(m_overlay->viewToOverlayOffset()); |
| |
| bool regionChanged = !m_region || !(*m_region == *region); |
| m_region = WTFMove(region); |
| return regionChanged; |
| } |
| |
| class NonFastScrollableRegionOverlay final : public RegionOverlay { |
| public: |
| static Ref<NonFastScrollableRegionOverlay> create(Page& page) |
| { |
| return adoptRef(*new NonFastScrollableRegionOverlay(page)); |
| } |
| |
| private: |
| explicit NonFastScrollableRegionOverlay(Page& page) |
| : RegionOverlay(page, Color(1.0f, 0.5f, 0.0f, 0.4f)) |
| { |
| } |
| |
| bool updateRegion() override; |
| void drawRect(PageOverlay&, GraphicsContext&, const IntRect& dirtyRect) final; |
| |
| EventTrackingRegions m_eventTrackingRegions; |
| }; |
| |
| bool NonFastScrollableRegionOverlay::updateRegion() |
| { |
| bool regionChanged = false; |
| |
| if (ScrollingCoordinator* scrollingCoordinator = m_page.scrollingCoordinator()) { |
| EventTrackingRegions eventTrackingRegions = scrollingCoordinator->absoluteEventTrackingRegions(); |
| |
| if (eventTrackingRegions != m_eventTrackingRegions) { |
| m_eventTrackingRegions = eventTrackingRegions; |
| regionChanged = true; |
| } |
| } |
| |
| return regionChanged; |
| } |
| |
| static const HashMap<String, Color>& touchEventRegionColors() |
| { |
| static const auto regionColors = makeNeverDestroyed([] { |
| struct MapEntry { |
| ASCIILiteral name; |
| int r; |
| int g; |
| int b; |
| }; |
| static const MapEntry entries[] = { |
| { "touchstart"_s, 191, 191, 63 }, |
| { "touchmove"_s, 80, 204, 245 }, |
| { "touchend"_s, 191, 63, 127 }, |
| { "touchforcechange"_s, 63, 63, 191 }, |
| { "wheel"_s, 255, 128, 0 }, |
| { "mousedown"_s, 80, 245, 80 }, |
| { "mousemove"_s, 245, 245, 80 }, |
| { "mouseup"_s, 80, 245, 176 }, |
| }; |
| HashMap<String, Color> map; |
| for (auto& entry : entries) |
| map.add(entry.name, Color { entry.r, entry.g, entry.b, 50 }); |
| return map; |
| }()); |
| return regionColors; |
| } |
| |
| static void drawRightAlignedText(const String& text, GraphicsContext& context, const FontCascade& font, const FloatPoint& boxLocation) |
| { |
| float textGap = 10; |
| float textBaselineFromTop = 14; |
| |
| TextRun textRun = TextRun(text); |
| context.setFillColor(Color::transparent); |
| float textWidth = context.drawText(font, textRun, { }); |
| context.setFillColor(Color::black); |
| context.drawText(font, textRun, boxLocation + FloatSize(-(textWidth + textGap), textBaselineFromTop)); |
| } |
| |
| void NonFastScrollableRegionOverlay::drawRect(PageOverlay& pageOverlay, GraphicsContext& context, const IntRect&) |
| { |
| IntRect bounds = pageOverlay.bounds(); |
| |
| context.clearRect(bounds); |
| |
| FloatRect legendRect = { bounds.maxX() - 30.0f, 10, 20, 20 }; |
| |
| FontCascadeDescription fontDescription; |
| fontDescription.setOneFamily("Helvetica"); |
| fontDescription.setSpecifiedSize(12); |
| fontDescription.setComputedSize(12); |
| fontDescription.setWeight(FontSelectionValue(500)); |
| FontCascade font(WTFMove(fontDescription), 0, 0); |
| font.update(nullptr); |
| |
| #if ENABLE(TOUCH_EVENTS) |
| context.setFillColor(touchEventRegionColors().get("touchstart")); |
| context.fillRect(legendRect); |
| drawRightAlignedText("touchstart", context, font, legendRect.location()); |
| |
| legendRect.move(0, 30); |
| context.setFillColor(touchEventRegionColors().get("touchmove")); |
| context.fillRect(legendRect); |
| drawRightAlignedText("touchmove", context, font, legendRect.location()); |
| |
| legendRect.move(0, 30); |
| context.setFillColor(touchEventRegionColors().get("touchend")); |
| context.fillRect(legendRect); |
| drawRightAlignedText("touchend", context, font, legendRect.location()); |
| |
| legendRect.move(0, 30); |
| context.setFillColor(touchEventRegionColors().get("touchforcechange")); |
| context.fillRect(legendRect); |
| drawRightAlignedText("touchforcechange", context, font, legendRect.location()); |
| |
| legendRect.move(0, 30); |
| context.setFillColor(m_color); |
| context.fillRect(legendRect); |
| drawRightAlignedText("passive listeners", context, font, legendRect.location()); |
| |
| legendRect.move(0, 30); |
| context.setFillColor(touchEventRegionColors().get("mousedown")); |
| context.fillRect(legendRect); |
| drawRightAlignedText("mousedown", context, font, legendRect.location()); |
| |
| legendRect.move(0, 30); |
| context.setFillColor(touchEventRegionColors().get("mousemove")); |
| context.fillRect(legendRect); |
| drawRightAlignedText("mousemove", context, font, legendRect.location()); |
| |
| legendRect.move(0, 30); |
| context.setFillColor(touchEventRegionColors().get("mouseup")); |
| context.fillRect(legendRect); |
| drawRightAlignedText("mouseup", context, font, legendRect.location()); |
| #else |
| // On desktop platforms, the "wheel" region includes the non-fast scrollable region. |
| context.setFillColor(touchEventRegionColors().get("wheel")); |
| context.fillRect(legendRect); |
| drawRightAlignedText("non-fast region", context, font, legendRect.location()); |
| #endif |
| |
| for (const auto& synchronousEventRegion : m_eventTrackingRegions.eventSpecificSynchronousDispatchRegions) { |
| Color regionColor(0, 0, 0, 64); |
| auto it = touchEventRegionColors().find(synchronousEventRegion.key); |
| if (it != touchEventRegionColors().end()) |
| regionColor = it->value; |
| drawRegion(context, synchronousEventRegion.value, regionColor, bounds); |
| } |
| |
| drawRegion(context, m_eventTrackingRegions.asynchronousDispatchRegion, m_color, bounds); |
| } |
| |
| Ref<RegionOverlay> RegionOverlay::create(Page& page, DebugPageOverlays::RegionType regionType) |
| { |
| switch (regionType) { |
| case DebugPageOverlays::RegionType::WheelEventHandlers: |
| return MouseWheelRegionOverlay::create(page); |
| case DebugPageOverlays::RegionType::NonFastScrollableRegion: |
| return NonFastScrollableRegionOverlay::create(page); |
| } |
| ASSERT_NOT_REACHED(); |
| return MouseWheelRegionOverlay::create(page); |
| } |
| |
| RegionOverlay::RegionOverlay(Page& page, Color regionColor) |
| : m_page(page) |
| , m_overlay(PageOverlay::create(*this, PageOverlay::OverlayType::Document)) |
| , m_color(regionColor) |
| { |
| } |
| |
| RegionOverlay::~RegionOverlay() |
| { |
| if (m_overlay) |
| m_page.pageOverlayController().uninstallPageOverlay(*m_overlay, PageOverlay::FadeMode::DoNotFade); |
| } |
| |
| void RegionOverlay::willMoveToPage(PageOverlay&, Page* page) |
| { |
| if (!page) |
| m_overlay = nullptr; |
| } |
| |
| void RegionOverlay::didMoveToPage(PageOverlay&, Page* page) |
| { |
| if (page) |
| recomputeRegion(); |
| } |
| |
| void RegionOverlay::drawRect(PageOverlay&, GraphicsContext& context, const IntRect& dirtyRect) |
| { |
| context.clearRect(dirtyRect); |
| |
| if (!m_region) |
| return; |
| |
| drawRegion(context, *m_region, m_color, dirtyRect); |
| } |
| |
| void RegionOverlay::drawRegion(GraphicsContext& context, const Region& region, const Color& color, const IntRect& dirtyRect) |
| { |
| GraphicsContextStateSaver saver(context); |
| context.setFillColor(color); |
| for (auto rect : region.rects()) { |
| if (rect.intersects(dirtyRect)) |
| context.fillRect(rect); |
| } |
| } |
| |
| bool RegionOverlay::mouseEvent(PageOverlay&, const PlatformMouseEvent&) |
| { |
| return false; |
| } |
| |
| void RegionOverlay::didScrollFrame(PageOverlay&, Frame&) |
| { |
| } |
| |
| void RegionOverlay::recomputeRegion() |
| { |
| if (updateRegion()) |
| m_overlay->setNeedsDisplay(); |
| } |
| |
| DebugPageOverlays& DebugPageOverlays::singleton() |
| { |
| if (!sharedDebugOverlays) |
| sharedDebugOverlays = new DebugPageOverlays; |
| |
| return *sharedDebugOverlays; |
| } |
| |
| static inline size_t indexOf(DebugPageOverlays::RegionType regionType) |
| { |
| return static_cast<size_t>(regionType); |
| } |
| |
| RegionOverlay& DebugPageOverlays::ensureRegionOverlayForPage(Page& page, RegionType regionType) |
| { |
| auto it = m_pageRegionOverlays.find(&page); |
| if (it != m_pageRegionOverlays.end()) { |
| auto& visualizer = it->value[indexOf(regionType)]; |
| if (!visualizer) |
| visualizer = RegionOverlay::create(page, regionType); |
| return *visualizer; |
| } |
| |
| Vector<RefPtr<RegionOverlay>> visualizers(NumberOfRegionTypes); |
| auto visualizer = RegionOverlay::create(page, regionType); |
| visualizers[indexOf(regionType)] = visualizer.copyRef(); |
| m_pageRegionOverlays.add(&page, WTFMove(visualizers)); |
| return visualizer; |
| } |
| |
| void DebugPageOverlays::showRegionOverlay(Page& page, RegionType regionType) |
| { |
| auto& visualizer = ensureRegionOverlayForPage(page, regionType); |
| page.pageOverlayController().installPageOverlay(visualizer.overlay(), PageOverlay::FadeMode::DoNotFade); |
| } |
| |
| void DebugPageOverlays::hideRegionOverlay(Page& page, RegionType regionType) |
| { |
| auto it = m_pageRegionOverlays.find(&page); |
| if (it == m_pageRegionOverlays.end()) |
| return; |
| auto& visualizer = it->value[indexOf(regionType)]; |
| if (!visualizer) |
| return; |
| page.pageOverlayController().uninstallPageOverlay(visualizer->overlay(), PageOverlay::FadeMode::DoNotFade); |
| visualizer = nullptr; |
| } |
| |
| void DebugPageOverlays::regionChanged(Frame& frame, RegionType regionType) |
| { |
| auto* page = frame.page(); |
| if (!page) |
| return; |
| |
| if (auto* visualizer = regionOverlayForPage(*page, regionType)) |
| visualizer->recomputeRegion(); |
| } |
| |
| RegionOverlay* DebugPageOverlays::regionOverlayForPage(Page& page, RegionType regionType) const |
| { |
| auto it = m_pageRegionOverlays.find(&page); |
| if (it == m_pageRegionOverlays.end()) |
| return nullptr; |
| return it->value.at(indexOf(regionType)).get(); |
| } |
| |
| void DebugPageOverlays::updateOverlayRegionVisibility(Page& page, DebugOverlayRegions visibleRegions) |
| { |
| if (visibleRegions & NonFastScrollableRegion) |
| showRegionOverlay(page, RegionType::NonFastScrollableRegion); |
| else |
| hideRegionOverlay(page, RegionType::NonFastScrollableRegion); |
| |
| if (visibleRegions & WheelEventHandlerRegion) |
| showRegionOverlay(page, RegionType::WheelEventHandlers); |
| else |
| hideRegionOverlay(page, RegionType::WheelEventHandlers); |
| } |
| |
| void DebugPageOverlays::settingsChanged(Page& page) |
| { |
| DebugOverlayRegions activeOverlayRegions = page.settings().visibleDebugOverlayRegions(); |
| if (!activeOverlayRegions && !hasOverlays(page)) |
| return; |
| |
| DebugPageOverlays::singleton().updateOverlayRegionVisibility(page, activeOverlayRegions); |
| } |
| |
| } |