HitTestResult.cpp   [plain text]


/*
 * Copyright (C) 2006, 2008, 2011 Apple Inc. All rights reserved.
 * Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
*/

#include "config.h"
#include "HitTestResult.h"

#include "CachedImage.h"
#include "DocumentMarkerController.h"
#include "Editor.h"
#include "File.h"
#include "Frame.h"
#include "FrameSelection.h"
#include "FrameTree.h"
#include "HTMLAnchorElement.h"
#include "HTMLAttachmentElement.h"
#include "HTMLEmbedElement.h"
#include "HTMLImageElement.h"
#include "HTMLInputElement.h"
#include "HTMLMediaElement.h"
#include "HTMLNames.h"
#include "HTMLObjectElement.h"
#include "HTMLParserIdioms.h"
#include "HTMLPlugInImageElement.h"
#include "HTMLTextAreaElement.h"
#include "HTMLVideoElement.h"
#include "HitTestLocation.h"
#include "PseudoElement.h"
#include "RenderBlockFlow.h"
#include "RenderImage.h"
#include "RenderInline.h"
#include "SVGAElement.h"
#include "SVGImageElement.h"
#include "Scrollbar.h"
#include "ShadowRoot.h"
#include "TextIterator.h"
#include "UserGestureIndicator.h"
#include "VisibleUnits.h"
#include "XLinkNames.h"

namespace WebCore {

using namespace HTMLNames;

HitTestResult::HitTestResult()
    : m_isOverWidget(false)
{
}

HitTestResult::HitTestResult(const LayoutPoint& point)
    : m_hitTestLocation(point)
    , m_pointInInnerNodeFrame(point)
    , m_isOverWidget(false)
{
}

HitTestResult::HitTestResult(const LayoutPoint& centerPoint, unsigned topPadding, unsigned rightPadding, unsigned bottomPadding, unsigned leftPadding)
    : m_hitTestLocation(centerPoint, topPadding, rightPadding, bottomPadding, leftPadding)
    , m_pointInInnerNodeFrame(centerPoint)
    , m_isOverWidget(false)
{
}

HitTestResult::HitTestResult(const HitTestLocation& other)
    : m_hitTestLocation(other)
    , m_pointInInnerNodeFrame(m_hitTestLocation.point())
    , m_isOverWidget(false)
{
}

HitTestResult::HitTestResult(const HitTestResult& other)
    : m_hitTestLocation(other.m_hitTestLocation)
    , m_innerNode(other.innerNode())
    , m_innerNonSharedNode(other.innerNonSharedNode())
    , m_pointInInnerNodeFrame(other.m_pointInInnerNodeFrame)
    , m_localPoint(other.localPoint())
    , m_innerURLElement(other.URLElement())
    , m_scrollbar(other.scrollbar())
    , m_isOverWidget(other.isOverWidget())
{
    // Only copy the NodeSet in case of list hit test.
    m_listBasedTestResult = other.m_listBasedTestResult ? std::make_unique<NodeSet>(*other.m_listBasedTestResult) : nullptr;
}

HitTestResult::~HitTestResult() = default;

HitTestResult& HitTestResult::operator=(const HitTestResult& other)
{
    m_hitTestLocation = other.m_hitTestLocation;
    m_innerNode = other.innerNode();
    m_innerNonSharedNode = other.innerNonSharedNode();
    m_pointInInnerNodeFrame = other.m_pointInInnerNodeFrame;
    m_localPoint = other.localPoint();
    m_innerURLElement = other.URLElement();
    m_scrollbar = other.scrollbar();
    m_isOverWidget = other.isOverWidget();

    // Only copy the NodeSet in case of list hit test.
    m_listBasedTestResult = other.m_listBasedTestResult ? std::make_unique<NodeSet>(*other.m_listBasedTestResult) : nullptr;

    return *this;
}

static Node* moveOutOfUserAgentShadowTree(Node& node)
{
    if (node.isInShadowTree()) {
        if (ShadowRoot* root = node.containingShadowRoot()) {
            if (root->mode() == ShadowRootMode::UserAgent)
                return root->host();
        }
    }
    return &node;
}

void HitTestResult::setToNonUserAgentShadowAncestor()
{
    if (Node* node = innerNode()) {
        node = moveOutOfUserAgentShadowTree(*node);
        setInnerNode(node);
    }
    if (Node *node = innerNonSharedNode()) {
        node = moveOutOfUserAgentShadowTree(*node);
        setInnerNonSharedNode(node);
    }
}

void HitTestResult::setInnerNode(Node* node)
{
    if (is<PseudoElement>(node))
        node = downcast<PseudoElement>(*node).hostElement();
    m_innerNode = node;
}
    
void HitTestResult::setInnerNonSharedNode(Node* node)
{
    if (is<PseudoElement>(node))
        node = downcast<PseudoElement>(*node).hostElement();
    m_innerNonSharedNode = node;
}

void HitTestResult::setURLElement(Element* n) 
{ 
    m_innerURLElement = n; 
}

void HitTestResult::setScrollbar(Scrollbar* s)
{
    m_scrollbar = s;
}

Frame* HitTestResult::innerNodeFrame() const
{
    if (m_innerNonSharedNode)
        return m_innerNonSharedNode->document().frame();
    if (m_innerNode)
        return m_innerNode->document().frame();
    return 0;
}

Frame* HitTestResult::targetFrame() const
{
    if (!m_innerURLElement)
        return 0;

    Frame* frame = m_innerURLElement->document().frame();
    if (!frame)
        return 0;

    return frame->tree().find(m_innerURLElement->target());
}

bool HitTestResult::isSelected() const
{
    if (!m_innerNonSharedNode)
        return false;

    Frame* frame = m_innerNonSharedNode->document().frame();
    if (!frame)
        return false;

    return frame->selection().contains(m_hitTestLocation.point());
}

String HitTestResult::selectedText() const
{
    if (!m_innerNonSharedNode)
        return emptyString();

    Frame* frame = m_innerNonSharedNode->document().frame();
    if (!frame)
        return emptyString();

    // Look for a character that's not just a separator.
    for (TextIterator it(frame->selection().toNormalizedRange().get()); !it.atEnd(); it.advance()) {
        int length = it.text().length();
        for (int i = 0; i < length; ++i) {
            if (!(U_GET_GC_MASK(it.text()[i]) & U_GC_Z_MASK))
                return frame->displayStringModifiedByEncoding(frame->editor().selectedText());
        }
    }
    return emptyString();
}

String HitTestResult::spellingToolTip(TextDirection& dir) const
{
    dir = LTR;
    // Return the tool tip string associated with this point, if any. Only markers associated with bad grammar
    // currently supply strings, but maybe someday markers associated with misspelled words will also.
    if (!m_innerNonSharedNode)
        return String();
    
    DocumentMarker* marker = m_innerNonSharedNode->document().markers().markerContainingPoint(m_hitTestLocation.point(), DocumentMarker::Grammar);
    if (!marker)
        return String();

    if (auto renderer = m_innerNonSharedNode->renderer())
        dir = renderer->style().direction();
    return marker->description();
}

String HitTestResult::replacedString() const
{
    // Return the replaced string associated with this point, if any. This marker is created when a string is autocorrected, 
    // and is used for generating a contextual menu item that allows it to easily be changed back if desired.
    if (!m_innerNonSharedNode)
        return String();
    
    DocumentMarker* marker = m_innerNonSharedNode->document().markers().markerContainingPoint(m_hitTestLocation.point(), DocumentMarker::Replacement);
    if (!marker)
        return String();
    
    return marker->description();
}    
    
String HitTestResult::title(TextDirection& dir) const
{
    dir = LTR;
    // Find the title in the nearest enclosing DOM node.
    // For <area> tags in image maps, walk the tree for the <area>, not the <img> using it.
    for (Node* titleNode = m_innerNode.get(); titleNode; titleNode = titleNode->parentInComposedTree()) {
        if (is<Element>(*titleNode)) {
            Element& titleElement = downcast<Element>(*titleNode);
            String title = titleElement.title();
            if (!title.isEmpty()) {
                if (auto renderer = titleElement.renderer())
                    dir = renderer->style().direction();
                return title;
            }
        }
    }
    return String();
}

String HitTestResult::innerTextIfTruncated(TextDirection& dir) const
{
    for (Node* truncatedNode = m_innerNode.get(); truncatedNode; truncatedNode = truncatedNode->parentInComposedTree()) {
        if (!is<Element>(*truncatedNode))
            continue;

        if (auto renderer = downcast<Element>(*truncatedNode).renderer()) {
            if (is<RenderBlockFlow>(*renderer)) {
                RenderBlockFlow& block = downcast<RenderBlockFlow>(*renderer);
                if (block.style().textOverflow() == TextOverflow::Ellipsis) {
                    for (RootInlineBox* line = block.firstRootBox(); line; line = line->nextRootBox()) {
                        if (line->hasEllipsisBox()) {
                            dir = block.style().direction();
                            return downcast<Element>(*truncatedNode).innerText();
                        }
                    }
                }
                break;
            }
        }
    }

    dir = LTR;
    return String();
}

String displayString(const String& string, const Node* node)
{
    if (!node)
        return string;
    return node->document().displayStringModifiedByEncoding(string);
}

String HitTestResult::altDisplayString() const
{
    if (!m_innerNonSharedNode)
        return String();
    
    if (is<HTMLImageElement>(*m_innerNonSharedNode)) {
        HTMLImageElement& image = downcast<HTMLImageElement>(*m_innerNonSharedNode);
        return displayString(image.attributeWithoutSynchronization(altAttr), m_innerNonSharedNode.get());
    }
    
    if (is<HTMLInputElement>(*m_innerNonSharedNode)) {
        HTMLInputElement& input = downcast<HTMLInputElement>(*m_innerNonSharedNode);
        return displayString(input.alt(), m_innerNonSharedNode.get());
    }

    return String();
}

Image* HitTestResult::image() const
{
    if (!m_innerNonSharedNode)
        return nullptr;
    
    auto* renderer = m_innerNonSharedNode->renderer();
    if (is<RenderImage>(renderer)) {
        auto& image = downcast<RenderImage>(*renderer);
        if (image.cachedImage() && !image.cachedImage()->errorOccurred())
            return image.cachedImage()->imageForRenderer(&image);
    }

    return nullptr;
}

IntRect HitTestResult::imageRect() const
{
    if (!image())
        return IntRect();
    return m_innerNonSharedNode->renderBox()->absoluteContentQuad().enclosingBoundingBox();
}

URL HitTestResult::absoluteImageURL() const
{
    if (!m_innerNonSharedNode)
        return URL();

    if (!(m_innerNonSharedNode->renderer() && m_innerNonSharedNode->renderer()->isImage()))
        return URL();

    AtomicString urlString;
    if (is<HTMLEmbedElement>(*m_innerNonSharedNode)
        || is<HTMLImageElement>(*m_innerNonSharedNode)
        || is<HTMLInputElement>(*m_innerNonSharedNode)
        || is<HTMLObjectElement>(*m_innerNonSharedNode)
        || is<SVGImageElement>(*m_innerNonSharedNode)) {
        urlString = downcast<Element>(*m_innerNonSharedNode).imageSourceURL();
    } else
        return URL();

    return m_innerNonSharedNode->document().completeURL(stripLeadingAndTrailingHTMLSpaces(urlString));
}

URL HitTestResult::absolutePDFURL() const
{
    if (!m_innerNonSharedNode)
        return URL();

    if (!is<HTMLEmbedElement>(*m_innerNonSharedNode) && !is<HTMLObjectElement>(*m_innerNonSharedNode))
        return URL();

    HTMLPlugInImageElement& element = downcast<HTMLPlugInImageElement>(*m_innerNonSharedNode);
    URL url = m_innerNonSharedNode->document().completeURL(stripLeadingAndTrailingHTMLSpaces(element.url()));
    if (!url.isValid())
        return URL();

    if (element.serviceType() == "application/pdf" || (element.serviceType().isEmpty() && url.path().endsWithIgnoringASCIICase(".pdf")))
        return url;
    return URL();
}

URL HitTestResult::absoluteMediaURL() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        return mediaElt->currentSrc();
    return URL();
#else
    return URL();
#endif
}

bool HitTestResult::mediaSupportsFullscreen() const
{
#if ENABLE(VIDEO)
    HTMLMediaElement* mediaElt(mediaElement());
    return is<HTMLVideoElement>(mediaElt) && mediaElt->supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModeStandard);
#else
    return false;
#endif
}

#if ENABLE(VIDEO)
HTMLMediaElement* HitTestResult::mediaElement() const
{
    if (!m_innerNonSharedNode)
        return nullptr;

    if (!(m_innerNonSharedNode->renderer() && m_innerNonSharedNode->renderer()->isMedia()))
        return nullptr;

    if (is<HTMLMediaElement>(*m_innerNonSharedNode))
        return downcast<HTMLMediaElement>(m_innerNonSharedNode.get());
    return nullptr;
}
#endif

void HitTestResult::toggleMediaControlsDisplay() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        mediaElt->setControls(!mediaElt->controls());
#endif
}

void HitTestResult::toggleMediaLoopPlayback() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        mediaElt->setLoop(!mediaElt->loop());
#endif
}

bool HitTestResult::mediaIsInFullscreen() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElement = this->mediaElement())
        return mediaElement->isVideo() && mediaElement->isStandardFullscreen();
#endif
    return false;
}

void HitTestResult::toggleMediaFullscreenState() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElement = this->mediaElement()) {
        if (mediaElement->isVideo() && mediaElement->supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModeStandard)) {
            UserGestureIndicator indicator(ProcessingUserGesture, &mediaElement->document());
            mediaElement->toggleStandardFullscreenState();
        }
    }
#endif
}

void HitTestResult::enterFullscreenForVideo() const
{
#if ENABLE(VIDEO)
    HTMLMediaElement* mediaElement(this->mediaElement());
    if (is<HTMLVideoElement>(mediaElement)) {
        HTMLVideoElement& videoElement = downcast<HTMLVideoElement>(*mediaElement);
        if (!videoElement.isFullscreen() && mediaElement->supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModeStandard)) {
            UserGestureIndicator indicator(ProcessingUserGesture, &mediaElement->document());
            videoElement.enterFullscreen();
        }
    }
#endif
}

bool HitTestResult::mediaControlsEnabled() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElement = this->mediaElement())
        return mediaElement->controls();
#endif
    return false;
}

bool HitTestResult::mediaLoopEnabled() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        return mediaElt->loop();
#endif
    return false;
}

bool HitTestResult::mediaPlaying() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        return !mediaElt->paused();
#endif
    return false;
}

void HitTestResult::toggleMediaPlayState() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        mediaElt->togglePlayState();
#endif
}

bool HitTestResult::mediaHasAudio() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        return mediaElt->hasAudio();
#endif
    return false;
}

bool HitTestResult::mediaIsVideo() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        return is<HTMLVideoElement>(*mediaElt);
#endif
    return false;
}

bool HitTestResult::mediaMuted() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        return mediaElt->muted();
#endif
    return false;
}

void HitTestResult::toggleMediaMuteState() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        mediaElt->setMuted(!mediaElt->muted());
#endif
}

bool HitTestResult::isDownloadableMedia() const
{
#if ENABLE(VIDEO)
    if (HTMLMediaElement* mediaElt = mediaElement())
        return mediaElt->canSaveMediaData();
#endif

    return false;
}

bool HitTestResult::isOverTextInsideFormControlElement() const
{
    Node* node = innerNode();
    if (!node)
        return false;

    if (!is<Element>(*node) || !downcast<Element>(*node).isTextField())
        return false;

    Frame* frame = node->document().frame();
    if (!frame)
        return false;

    IntPoint framePoint = roundedPointInInnerNodeFrame();
    if (!frame->rangeForPoint(framePoint))
        return false;

    VisiblePosition position = frame->visiblePositionForPoint(framePoint);
    if (position.isNull())
        return false;

    RefPtr<Range> wordRange = enclosingTextUnitOfGranularity(position, WordGranularity, DirectionForward);
    if (!wordRange)
        return false;

    return !wordRange->text().isEmpty();
}

URL HitTestResult::absoluteLinkURL() const
{
    if (m_innerURLElement)
        return m_innerURLElement->absoluteLinkURL();
    return URL();
}

bool HitTestResult::isOverLink() const
{
    return m_innerURLElement && m_innerURLElement->isLink();
}

String HitTestResult::titleDisplayString() const
{
    if (!m_innerURLElement)
        return String();
    
    return displayString(m_innerURLElement->title(), m_innerURLElement.get());
}

String HitTestResult::textContent() const
{
    if (!m_innerURLElement)
        return String();
    return m_innerURLElement->textContent();
}

// FIXME: This function needs a better name and may belong in a different class. It's not
// really isContentEditable(); it's more like needsEditingContextMenu(). In many ways, this
// function would make more sense in the ContextMenu class, except that WebElementDictionary 
// hooks into it. Anyway, we should architect this better. 
bool HitTestResult::isContentEditable() const
{
    if (!m_innerNonSharedNode)
        return false;

    if (is<HTMLTextAreaElement>(*m_innerNonSharedNode))
        return true;

    if (is<HTMLInputElement>(*m_innerNonSharedNode))
        return downcast<HTMLInputElement>(*m_innerNonSharedNode).isTextField();

    return m_innerNonSharedNode->hasEditableStyle();
}

HitTestProgress HitTestResult::addNodeToListBasedTestResult(Node* node, const HitTestRequest& request, const HitTestLocation& locationInContainer, const LayoutRect& rect)
{
    // If it is not a list-based hit test, this method has to be no-op.
    if (!request.resultIsElementList()) {
        ASSERT(!isRectBasedTest());
        return HitTestProgress::Stop;
    }

    if (!node)
        return HitTestProgress::Continue;

    if (request.disallowsUserAgentShadowContent() && node->isInUserAgentShadowTree())
        node = node->document().ancestorNodeInThisScope(node);

    mutableListBasedTestResult().add(node);

    if (request.includesAllElementsUnderPoint())
        return HitTestProgress::Continue;

    bool regionFilled = rect.contains(locationInContainer.boundingBox());
    return regionFilled ? HitTestProgress::Stop : HitTestProgress::Continue;
}

HitTestProgress HitTestResult::addNodeToListBasedTestResult(Node* node, const HitTestRequest& request, const HitTestLocation& locationInContainer, const FloatRect& rect)
{
    // If it is not a list-based hit test, this method has to be no-op.
    if (!request.resultIsElementList()) {
        ASSERT(!isRectBasedTest());
        return HitTestProgress::Stop;
    }

    if (!node)
        return HitTestProgress::Continue;

    if (request.disallowsUserAgentShadowContent() && node->isInUserAgentShadowTree())
        node = node->document().ancestorNodeInThisScope(node);

    mutableListBasedTestResult().add(node);

    if (request.includesAllElementsUnderPoint())
        return HitTestProgress::Continue;

    bool regionFilled = rect.contains(locationInContainer.boundingBox());
    return regionFilled ? HitTestProgress::Stop : HitTestProgress::Continue;
}

void HitTestResult::append(const HitTestResult& other, const HitTestRequest& request)
{
    ASSERT_UNUSED(request, request.resultIsElementList());

    if (!m_innerNode && other.innerNode()) {
        m_innerNode = other.innerNode();
        m_innerNonSharedNode = other.innerNonSharedNode();
        m_localPoint = other.localPoint();
        m_pointInInnerNodeFrame = other.m_pointInInnerNodeFrame;
        m_innerURLElement = other.URLElement();
        m_scrollbar = other.scrollbar();
        m_isOverWidget = other.isOverWidget();
    }

    if (other.m_listBasedTestResult) {
        NodeSet& set = mutableListBasedTestResult();
        for (auto node : *other.m_listBasedTestResult)
            set.add(node.get());
    }
}

const HitTestResult::NodeSet& HitTestResult::listBasedTestResult() const
{
    if (!m_listBasedTestResult)
        m_listBasedTestResult = std::make_unique<NodeSet>();
    return *m_listBasedTestResult;
}

HitTestResult::NodeSet& HitTestResult::mutableListBasedTestResult()
{
    if (!m_listBasedTestResult)
        m_listBasedTestResult = std::make_unique<NodeSet>();
    return *m_listBasedTestResult;
}

Vector<String> HitTestResult::dictationAlternatives() const
{
    // Return the dictation context handle if the text at this point has DictationAlternative marker, which means this text is
    if (!m_innerNonSharedNode)
        return Vector<String>();

    DocumentMarker* marker = m_innerNonSharedNode->document().markers().markerContainingPoint(pointInInnerNodeFrame(), DocumentMarker::DictationAlternatives);
    if (!marker)
        return Vector<String>();

    Frame* frame = innerNonSharedNode()->document().frame();
    if (!frame)
        return Vector<String>();

    return frame->editor().dictationAlternativesForMarker(*marker);
}

Node* HitTestResult::targetNode() const
{
    Node* node = innerNode();
    if (!node)
        return 0;
    if (node->isConnected())
        return node;

    Element* element = node->parentElement();
    if (element && element->isConnected())
        return element;

    return node;
}

Element* HitTestResult::targetElement() const
{
    for (Node* node = m_innerNode.get(); node; node = node->parentInComposedTree()) {
        if (is<Element>(*node))
            return downcast<Element>(node);
    }
    return nullptr;
}

Element* HitTestResult::innerNonSharedElement() const
{
    Node* node = m_innerNonSharedNode.get();
    if (!node)
        return nullptr;
    if (is<Element>(*node))
        return downcast<Element>(node);
    return node->parentElement();
}

String HitTestResult::linkSuggestedFilename() const
{
    auto* urlElement = URLElement();
    if (!is<HTMLAnchorElement>(urlElement))
        return nullAtom();
    return ResourceResponse::sanitizeSuggestedFilename(urlElement->attributeWithoutSynchronization(HTMLNames::downloadAttr));
}

bool HitTestResult::mediaSupportsEnhancedFullscreen() const
{
#if PLATFORM(MAC) && ENABLE(VIDEO) && ENABLE(VIDEO_PRESENTATION_MODE)
    HTMLMediaElement* mediaElt(mediaElement());
    return is<HTMLVideoElement>(mediaElt) && mediaElt->supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModePictureInPicture);
#else
    return false;
#endif
}

bool HitTestResult::mediaIsInEnhancedFullscreen() const
{
#if PLATFORM(MAC) && ENABLE(VIDEO) && ENABLE(VIDEO_PRESENTATION_MODE)
    HTMLMediaElement* mediaElt(mediaElement());
    return is<HTMLVideoElement>(mediaElt) && mediaElt->fullscreenMode() == HTMLMediaElementEnums::VideoFullscreenModePictureInPicture;
#else
    return false;
#endif
}

void HitTestResult::toggleEnhancedFullscreenForVideo() const
{
#if PLATFORM(MAC) && ENABLE(VIDEO) && ENABLE(VIDEO_PRESENTATION_MODE)
    HTMLMediaElement* mediaElement(this->mediaElement());
    if (!is<HTMLVideoElement>(mediaElement) || !mediaElement->supportsFullscreen(HTMLMediaElementEnums::VideoFullscreenModePictureInPicture))
        return;

    HTMLVideoElement& videoElement = downcast<HTMLVideoElement>(*mediaElement);
    UserGestureIndicator indicator(ProcessingUserGesture, &mediaElement->document());
    if (videoElement.fullscreenMode() == HTMLMediaElementEnums::VideoFullscreenModePictureInPicture)
        videoElement.exitFullscreen();
    else
        videoElement.enterFullscreen(HTMLMediaElementEnums::VideoFullscreenModePictureInPicture);
#endif
}

} // namespace WebCore